feat(backend-github): GitHub GraphQL API support (#2456)
* add GitHub GraphQL api initial support * support mutiple backends for e2e tests - initial commit * add github backend e2e test (currently skipped), fix bugs per tests * refactor e2e tests, add fork workflow tests, support fork workflow in GraphQL api * remove log message that might contain authentication token * return empty error when commit is not found when using GraphQL (align with base class) * disable github backend tests * fix bugs introduced after rebase of GraphQL and OpenAuthoring features * test: update tests per openAuthoring changes, split tests into multiple files * fix: pass in headers for pagination requests, avoid async iterator as it requires a polyfill on old browsers * test(e2e): disable github backend tests
This commit is contained in:
parent
083a336ba4
commit
ece136c92e
18
.eslintrc
18
.eslintrc
@ -1,14 +1,12 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:cypress/recommended"],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
"jest": true,
|
||||
"cypress/globals": true
|
||||
},
|
||||
"globals": {
|
||||
"NETLIFY_CMS_VERSION": false,
|
||||
@ -25,12 +23,10 @@
|
||||
"emotion/import-from-emotion": "error",
|
||||
"emotion/styled-import": "error"
|
||||
},
|
||||
"plugins": [
|
||||
"emotion",
|
||||
],
|
||||
"plugins": ["emotion", "cypress"],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect",
|
||||
},
|
||||
},
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,3 +15,5 @@ cypress/screenshots
|
||||
/coverage/
|
||||
.cache
|
||||
*.log
|
||||
.env
|
||||
.temp/
|
@ -1,4 +1,5 @@
|
||||
dist/
|
||||
bin/
|
||||
public/
|
||||
.cache/
|
||||
.cache/
|
||||
packages/netlify-cms-backend-github/src/fragmentTypes.js
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8080",
|
||||
"projectId": "dzqjxb"
|
||||
"projectId": "dzqjxb",
|
||||
"testFiles": "*spec*.js"
|
||||
}
|
||||
|
@ -1,349 +0,0 @@
|
||||
import '../utils/dismiss-local-backup';
|
||||
|
||||
describe('Editorial Workflow', () => {
|
||||
const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
|
||||
const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
|
||||
const entry1 = { title: 'first title', body: 'first body' };
|
||||
const entry2 = { title: 'second title', body: 'second body' };
|
||||
const entry3 = { title: 'third title', body: 'third body' };
|
||||
const setting1 = { limit: 10, author: 'John Doe' };
|
||||
const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
|
||||
const notifications = {
|
||||
saved: 'Entry saved',
|
||||
published: 'Entry published',
|
||||
updated: 'Entry status updated',
|
||||
deletedUnpublished: 'Unpublished changes deleted',
|
||||
error: {
|
||||
missingField: "Oops, you've missed a required field. Please complete before saving.",
|
||||
},
|
||||
validation: {
|
||||
range: {
|
||||
fieldLabel: 'Number of posts on frontpage',
|
||||
message: 'Number of posts on frontpage must be between 1 and 10.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Test Backend', () => {
|
||||
function login() {
|
||||
cy.viewport(1200, 1200);
|
||||
cy.visit('/');
|
||||
cy.contains('button', 'Login').click();
|
||||
}
|
||||
|
||||
function createPost({ title, body }) {
|
||||
cy.contains('a', 'New Post').click();
|
||||
cy.get('input')
|
||||
.first()
|
||||
.type(title);
|
||||
cy.get('[data-slate-editor]')
|
||||
.click()
|
||||
.type(body);
|
||||
cy.get('input')
|
||||
.first()
|
||||
.click();
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function validateObjectFields({ limit, author }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||
cy.get('input[type=number]').type(limit);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
cy.contains('label', 'Default Author').click();
|
||||
cy.focused().type(author);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function validateNestedObjectFields({ limit, author }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||
cy.contains('label', 'Default Author').click();
|
||||
cy.focused().type(author);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
cy.get('input[type=number]').type(limit + 1);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertFieldValidationError(notifications.validation.range);
|
||||
cy.get('input[type=number]')
|
||||
.clear()
|
||||
.type(-1);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertFieldValidationError(notifications.validation.range);
|
||||
cy.get('input[type=number]')
|
||||
.clear()
|
||||
.type(limit);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function validateListFields({ name, description }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/authors"]').click();
|
||||
cy.contains('button', 'Add').click();
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
cy.get('input')
|
||||
.eq(2)
|
||||
.type(name);
|
||||
cy.get('[data-slate-editor]')
|
||||
.eq(2)
|
||||
.type(description);
|
||||
cy.contains('button', 'Save').click();
|
||||
}
|
||||
|
||||
function exitEditor() {
|
||||
cy.contains('a[href^="#/collections/"]', 'Writing in').click();
|
||||
}
|
||||
|
||||
function deleteEntryInEditor() {
|
||||
cy.contains('button', 'Delete').click();
|
||||
assertNotification(notifications.deletedUnpublished);
|
||||
}
|
||||
|
||||
function assertEntryDeleted(entry) {
|
||||
if (Array.isArray(entry)) {
|
||||
const titles = entry.map(e => e.title);
|
||||
cy.get('a h2').each((el, idx) => {
|
||||
expect(titles).not.to.include(el.text());
|
||||
});
|
||||
} else {
|
||||
cy.get('a h2').each((el, idx) => {
|
||||
expect(entry.title).not.to.equal(el.text());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createAndDeletePost(entry) {
|
||||
createPost(entry);
|
||||
deleteEntryInEditor();
|
||||
}
|
||||
|
||||
function createPostAndExit(entry) {
|
||||
createPost(entry);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function createPublishedPost(entry) {
|
||||
createPost(entry);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
publishInEditor();
|
||||
}
|
||||
|
||||
function createPublishedPostAndExit(entry) {
|
||||
createPublishedPost(entry);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateObjectFieldsAndExit(setting) {
|
||||
validateObjectFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateNestedObjectFieldsAndExit(setting) {
|
||||
validateNestedObjectFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateListFieldsAndExit(setting) {
|
||||
validateListFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function goToWorkflow() {
|
||||
cy.contains('a', 'Workflow').click();
|
||||
}
|
||||
|
||||
function goToCollections() {
|
||||
cy.contains('a', 'Content').click();
|
||||
}
|
||||
|
||||
function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) {
|
||||
cy.contains('h2', fromColumnHeading)
|
||||
.parent()
|
||||
.contains('a', title)
|
||||
.trigger('dragstart', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
cy.contains('h2', toColumnHeading)
|
||||
.parent()
|
||||
.trigger('drop', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
assertNotification(notifications.updated);
|
||||
}
|
||||
|
||||
function assertWorkflowStatus({ title }, status) {
|
||||
cy.contains('h2', status)
|
||||
.parent()
|
||||
.contains('a', title);
|
||||
}
|
||||
|
||||
function updateWorkflowStatusInEditor(newStatus) {
|
||||
cy.contains('[role="button"]', 'Set status').as('setStatusButton');
|
||||
cy.get('@setStatusButton')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('@setStatusButton').click();
|
||||
cy.contains('[role="menuitem"] span', newStatus).click();
|
||||
});
|
||||
assertNotification(notifications.updated);
|
||||
}
|
||||
|
||||
function assertWorkflowStatusInEditor(status) {
|
||||
cy.contains('[role="button"]', 'Set status').as('setStatusButton');
|
||||
cy.get('@setStatusButton')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('@setStatusButton').click();
|
||||
cy.contains('[role="menuitem"] span', status)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('svg');
|
||||
});
|
||||
cy.get('@setStatusButton').click();
|
||||
});
|
||||
}
|
||||
|
||||
function publishWorkflowEntry({ title }) {
|
||||
cy.contains('h2', workflowStatus.ready)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('a', title)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('button', 'Publish new entry').click({ force: true });
|
||||
});
|
||||
});
|
||||
assertNotification(notifications.published);
|
||||
}
|
||||
|
||||
function assertPublishedEntry(entry) {
|
||||
if (Array.isArray(entry)) {
|
||||
const entries = entry.reverse();
|
||||
cy.get('a h2').then(els => {
|
||||
cy.wrap(els.slice(0, entries.length)).each((el, idx) => {
|
||||
cy.wrap(el).contains(entries[idx].title);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cy.get('a h2')
|
||||
.first()
|
||||
.contains(entry.title);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNotification(message) {
|
||||
if (Array.isArray(message)) {
|
||||
const messages = message.reverse();
|
||||
cy.get('.notif__container div')
|
||||
.should('have.length.of', messages.length)
|
||||
.each((el, idx) => {
|
||||
cy.wrap(el)
|
||||
.contains(messages[idx])
|
||||
.invoke('hide');
|
||||
});
|
||||
} else {
|
||||
cy.get('.notif__container').within(() => {
|
||||
cy.contains(message).invoke('hide');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function assertFieldValidationError({ message, fieldLabel }) {
|
||||
cy.contains('label', fieldLabel)
|
||||
.siblings('ul[class*=ControlErrorsList]')
|
||||
.contains(message);
|
||||
}
|
||||
|
||||
function assertOnCollectionsPage() {
|
||||
cy.url().should('contain', '/#/collections/posts');
|
||||
cy.contains('h2', 'Collections');
|
||||
}
|
||||
|
||||
it('successfully loads', () => {
|
||||
login();
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
});
|
||||
|
||||
it('can validate object fields', () => {
|
||||
login();
|
||||
validateObjectFieldsAndExit(setting1);
|
||||
});
|
||||
|
||||
it('can validate fields nested in an object field', () => {
|
||||
login();
|
||||
validateNestedObjectFieldsAndExit(setting1);
|
||||
});
|
||||
|
||||
it('can validate list fields', () => {
|
||||
login();
|
||||
validateListFieldsAndExit(setting2);
|
||||
});
|
||||
|
||||
it('can publish an editorial workflow entry', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry1);
|
||||
});
|
||||
|
||||
it('can change workflow status', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
|
||||
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review);
|
||||
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
});
|
||||
|
||||
it('can change status on and publish multiple entries', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
createPostAndExit(entry2);
|
||||
createPostAndExit(entry3);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry3);
|
||||
publishWorkflowEntry(entry2);
|
||||
publishWorkflowEntry(entry1);
|
||||
goToCollections();
|
||||
assertPublishedEntry([entry3, entry2, entry1]);
|
||||
});
|
||||
|
||||
it('can delete an entry', () => {
|
||||
login();
|
||||
createPost(entry1);
|
||||
deleteEntryInEditor();
|
||||
assertOnCollectionsPage();
|
||||
assertEntryDeleted(entry1);
|
||||
});
|
||||
|
||||
it('can update workflow status from within the editor', () => {
|
||||
login();
|
||||
createPost(entry1);
|
||||
assertWorkflowStatusInEditor(editorStatus.draft);
|
||||
updateWorkflowStatusInEditor(editorStatus.review);
|
||||
assertWorkflowStatusInEditor(editorStatus.review);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
assertWorkflowStatusInEditor(editorStatus.ready);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entry1, workflowStatus.ready);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
import fixture from './github/editorial_workflow';
|
||||
|
||||
describe.skip('Github Backend Editorial Workflow - GraphQL API', () => {
|
||||
fixture({ use_graphql: true });
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
import fixture from './github/open_authoring';
|
||||
|
||||
describe.skip('Github Backend Editorial Workflow - GraphQL API - Open Authoring', () => {
|
||||
fixture({ use_graphql: true });
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
import fixture from './github/editorial_workflow';
|
||||
|
||||
describe.skip('Github Backend Editorial Workflow - REST API', () => {
|
||||
fixture({ use_graphql: false });
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
import fixture from './github/open_authoring';
|
||||
|
||||
describe.skip('Github Backend Editorial Workflow - REST API - Open Authoring', () => {
|
||||
fixture({ use_graphql: false });
|
||||
});
|
126
cypress/integration/editorial_workflow_spec_test_backend.js
Normal file
126
cypress/integration/editorial_workflow_spec_test_backend.js
Normal file
@ -0,0 +1,126 @@
|
||||
import '../utils/dismiss-local-backup';
|
||||
import {
|
||||
login,
|
||||
createPost,
|
||||
createPostAndExit,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
goToCollections,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
assertWorkflowStatusInEditor,
|
||||
assertPublishedEntry,
|
||||
deleteEntryInEditor,
|
||||
assertOnCollectionsPage,
|
||||
assertEntryDeleted,
|
||||
assertWorkflowStatus,
|
||||
updateWorkflowStatusInEditor,
|
||||
validateObjectFieldsAndExit,
|
||||
validateNestedObjectFieldsAndExit,
|
||||
validateListFieldsAndExit,
|
||||
} from '../utils/steps';
|
||||
import { setting1, setting2, workflowStatus, editorStatus } from '../utils/constants';
|
||||
|
||||
const entry1 = {
|
||||
title: 'first title',
|
||||
body: 'first body',
|
||||
};
|
||||
const entry2 = {
|
||||
title: 'second title',
|
||||
body: 'second body',
|
||||
};
|
||||
const entry3 = {
|
||||
title: 'third title',
|
||||
body: 'third body',
|
||||
};
|
||||
|
||||
describe('Test Backend Editorial Workflow', () => {
|
||||
after(() => {
|
||||
cy.task('restoreDefaults');
|
||||
});
|
||||
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
login();
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
});
|
||||
|
||||
it('can validate object fields', () => {
|
||||
login();
|
||||
validateObjectFieldsAndExit(setting1);
|
||||
});
|
||||
|
||||
it('can validate fields nested in an object field', () => {
|
||||
login();
|
||||
validateNestedObjectFieldsAndExit(setting1);
|
||||
});
|
||||
|
||||
it('can validate list fields', () => {
|
||||
login();
|
||||
validateListFieldsAndExit(setting2);
|
||||
});
|
||||
|
||||
it('can publish an editorial workflow entry', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry1);
|
||||
});
|
||||
|
||||
it('can change workflow status', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
|
||||
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review);
|
||||
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
});
|
||||
|
||||
it('can change status on and publish multiple entries', () => {
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
createPostAndExit(entry2);
|
||||
createPostAndExit(entry3);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry3);
|
||||
publishWorkflowEntry(entry2);
|
||||
publishWorkflowEntry(entry1);
|
||||
goToCollections();
|
||||
assertPublishedEntry([entry3, entry2, entry1]);
|
||||
});
|
||||
|
||||
it('can delete an entry', () => {
|
||||
login();
|
||||
createPost(entry1);
|
||||
deleteEntryInEditor();
|
||||
assertOnCollectionsPage();
|
||||
assertEntryDeleted(entry1);
|
||||
});
|
||||
|
||||
it('can update workflow status from within the editor', () => {
|
||||
login();
|
||||
createPost(entry1);
|
||||
assertWorkflowStatusInEditor(editorStatus.draft);
|
||||
updateWorkflowStatusInEditor(editorStatus.review);
|
||||
assertWorkflowStatusInEditor(editorStatus.review);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
assertWorkflowStatusInEditor(editorStatus.ready);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entry1, workflowStatus.ready);
|
||||
});
|
||||
});
|
117
cypress/integration/github/editorial_workflow.js
Normal file
117
cypress/integration/github/editorial_workflow.js
Normal file
@ -0,0 +1,117 @@
|
||||
import '../../utils/dismiss-local-backup';
|
||||
import {
|
||||
login,
|
||||
createPost,
|
||||
createPostAndExit,
|
||||
updateExistingPostAndExit,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
goToCollections,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
assertWorkflowStatusInEditor,
|
||||
assertPublishedEntry,
|
||||
deleteEntryInEditor,
|
||||
assertOnCollectionsPage,
|
||||
assertEntryDeleted,
|
||||
assertWorkflowStatus,
|
||||
updateWorkflowStatusInEditor,
|
||||
} from '../../utils/steps';
|
||||
import { workflowStatus, editorStatus } from '../../utils/constants';
|
||||
import { entry1, entry2, entry3 } from './entries';
|
||||
|
||||
export default function({ use_graphql }) {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
before(() => {
|
||||
Cypress.config('taskTimeout', 1200000);
|
||||
Cypress.config('defaultCommandTimeout', 60000);
|
||||
cy.task('setupBackend', { backend }).then(data => {
|
||||
taskResult.data = data;
|
||||
});
|
||||
|
||||
cy.task('updateBackendOptions', { backend, options: { use_graphql, open_authoring: false } });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend, ...taskResult.data });
|
||||
cy.task('restoreDefaults');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.task('teardownBackendTest', { backend, ...taskResult.data });
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
login(taskResult.data.user);
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
});
|
||||
|
||||
it('can update an entry', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
updateExistingPostAndExit(entry1, entry2);
|
||||
});
|
||||
|
||||
it('can publish an editorial workflow entry', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry1);
|
||||
});
|
||||
|
||||
it('can change workflow status', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
|
||||
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review);
|
||||
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
});
|
||||
|
||||
it('can change status on and publish multiple entries', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
createPostAndExit(entry2);
|
||||
createPostAndExit(entry3);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry3);
|
||||
publishWorkflowEntry(entry2);
|
||||
publishWorkflowEntry(entry1);
|
||||
goToCollections();
|
||||
assertPublishedEntry([entry3, entry2, entry1]);
|
||||
});
|
||||
|
||||
it('can delete an entry', () => {
|
||||
login(taskResult.data.user);
|
||||
createPost(entry1);
|
||||
deleteEntryInEditor();
|
||||
assertOnCollectionsPage();
|
||||
assertEntryDeleted(entry1);
|
||||
});
|
||||
|
||||
it('can update workflow status from within the editor', () => {
|
||||
login(taskResult.data.user);
|
||||
createPost(entry1);
|
||||
assertWorkflowStatusInEditor(editorStatus.draft);
|
||||
updateWorkflowStatusInEditor(editorStatus.review);
|
||||
assertWorkflowStatusInEditor(editorStatus.review);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
assertWorkflowStatusInEditor(editorStatus.ready);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entry1, workflowStatus.ready);
|
||||
});
|
||||
}
|
21
cypress/integration/github/entries.js
Normal file
21
cypress/integration/github/entries.js
Normal file
@ -0,0 +1,21 @@
|
||||
export const entry1 = {
|
||||
title: 'first title',
|
||||
body: 'first body',
|
||||
description: 'first description',
|
||||
category: 'first category',
|
||||
tags: 'tag1',
|
||||
};
|
||||
export const entry2 = {
|
||||
title: 'second title',
|
||||
body: 'second body',
|
||||
description: 'second description',
|
||||
category: 'second category',
|
||||
tags: 'tag2',
|
||||
};
|
||||
export const entry3 = {
|
||||
title: 'third title',
|
||||
body: 'third body',
|
||||
description: 'third description',
|
||||
category: 'third category',
|
||||
tags: 'tag3',
|
||||
};
|
99
cypress/integration/github/open_authoring.js
Normal file
99
cypress/integration/github/open_authoring.js
Normal file
@ -0,0 +1,99 @@
|
||||
import '../../utils/dismiss-local-backup';
|
||||
import {
|
||||
login,
|
||||
createPostAndExit,
|
||||
updateExistingPostAndExit,
|
||||
goToWorkflow,
|
||||
deleteWorkflowEntry,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
} from '../../utils/steps';
|
||||
import { workflowStatus } from '../../utils/constants';
|
||||
import { entry1, entry2 } from './entries';
|
||||
|
||||
export default function({ use_graphql }) {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
before(() => {
|
||||
Cypress.config('taskTimeout', 1200000);
|
||||
Cypress.config('defaultCommandTimeout', 60000);
|
||||
cy.task('setupBackend', { backend }).then(data => {
|
||||
taskResult.data = data;
|
||||
});
|
||||
cy.task('updateBackendOptions', { backend, options: { use_graphql, open_authoring: true } });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend, ...taskResult.data });
|
||||
cy.task('restoreDefaults');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.task('teardownBackendTest', { backend, ...taskResult.data });
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
login(taskResult.data.user);
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
});
|
||||
|
||||
it('can update an entry', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
updateExistingPostAndExit(entry1, entry2);
|
||||
});
|
||||
|
||||
it('can publish an editorial workflow entry', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry1);
|
||||
});
|
||||
|
||||
it('successfully forks repository and loads', () => {
|
||||
login(taskResult.data.forkUser);
|
||||
});
|
||||
|
||||
it('can create an entry on fork', () => {
|
||||
login(taskResult.data.forkUser);
|
||||
createPostAndExit(entry1);
|
||||
});
|
||||
|
||||
it('can update a draft entry on fork', () => {
|
||||
login(taskResult.data.user);
|
||||
createPostAndExit(entry1);
|
||||
updateExistingPostAndExit(entry1, entry2);
|
||||
});
|
||||
|
||||
it('can change entry status from fork', () => {
|
||||
login(taskResult.data.forkUser);
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
|
||||
});
|
||||
|
||||
it('can delete review entry from fork', () => {
|
||||
login(taskResult.data.forkUser);
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
|
||||
deleteWorkflowEntry(entry1);
|
||||
});
|
||||
|
||||
it('can return entry to draft and delete it', () => {
|
||||
login(taskResult.data.forkUser);
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
|
||||
|
||||
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
|
||||
deleteWorkflowEntry(entry1);
|
||||
});
|
||||
}
|
193
cypress/plugins/github.js
Normal file
193
cypress/plugins/github.js
Normal file
@ -0,0 +1,193 @@
|
||||
const Octokit = require('@octokit/rest');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const simpleGit = require('simple-git/promise');
|
||||
|
||||
const GIT_SSH_COMMAND = 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no';
|
||||
|
||||
function getGitHubClient(token) {
|
||||
const client = new Octokit({
|
||||
auth: `token ${token}`,
|
||||
baseUrl: 'https://api.github.com',
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
function getEnvs() {
|
||||
const {
|
||||
GITHUB_REPO_OWNER: owner,
|
||||
GITHUB_REPO_NAME: repo,
|
||||
GITHUB_REPO_TOKEN: token,
|
||||
GITHUB_OPEN_AUTHORING_OWNER: forkOwner,
|
||||
GITHUB_OPEN_AUTHORING_TOKEN: forkToken,
|
||||
} = process.env;
|
||||
if (!owner || !repo || !token || !forkOwner || !forkToken) {
|
||||
throw new Error(
|
||||
'Please set GITHUB_REPO_OWNER, GITHUB_REPO_NAME, GITHUB_REPO_TOKEN, GITHUB_OPEN_AUTHORING_OWNER, GITHUB_OPEN_AUTHORING_TOKEN environment variables',
|
||||
);
|
||||
}
|
||||
return { owner, repo, token, forkOwner, forkToken };
|
||||
}
|
||||
|
||||
async function prepareTestGitHubRepo() {
|
||||
const { owner, repo, token } = getEnvs();
|
||||
|
||||
// postfix a random string to avoid collisions
|
||||
const postfix = Math.random()
|
||||
.toString(32)
|
||||
.substring(2);
|
||||
const testRepoName = `${repo}-${Date.now()}-${postfix}`;
|
||||
|
||||
const client = getGitHubClient(token);
|
||||
|
||||
console.log('Creating repository', testRepoName);
|
||||
await client.repos.createForAuthenticatedUser({
|
||||
name: testRepoName,
|
||||
});
|
||||
|
||||
const tempDir = path.join('.temp', testRepoName);
|
||||
await fs.remove(tempDir);
|
||||
let git = simpleGit().env({ ...process.env, GIT_SSH_COMMAND });
|
||||
|
||||
const repoUrl = `git@github.com:${owner}/${repo}.git`;
|
||||
|
||||
console.log('Cloning repository', repoUrl);
|
||||
await git.clone(repoUrl, tempDir);
|
||||
git = simpleGit(tempDir).env({ ...process.env, GIT_SSH_COMMAND });
|
||||
|
||||
console.log('Pushing to new repository', testRepoName);
|
||||
|
||||
await git.removeRemote('origin');
|
||||
await git.addRemote(
|
||||
'origin',
|
||||
`https://${token}:x-oauth-basic@github.com/${owner}/${testRepoName}`,
|
||||
);
|
||||
await git.push(['-u', 'origin', 'master']);
|
||||
|
||||
return { owner, repo: testRepoName, tempDir };
|
||||
}
|
||||
|
||||
async function getAuthenticatedUser(token) {
|
||||
const client = getGitHubClient(token);
|
||||
const { data: user } = await client.users.getAuthenticated();
|
||||
return { ...user, token, backendName: 'github' };
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
const { token } = getEnvs();
|
||||
return getAuthenticatedUser(token);
|
||||
}
|
||||
|
||||
async function getForkUser() {
|
||||
const { forkToken } = getEnvs();
|
||||
return getAuthenticatedUser(forkToken);
|
||||
}
|
||||
|
||||
async function deleteRepositories({ owner, repo, tempDir }) {
|
||||
const { forkOwner, token, forkToken } = getEnvs();
|
||||
|
||||
const errorHandler = e => {
|
||||
if (e.status !== 404) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Deleting repository', `${owner}/${repo}`);
|
||||
await fs.remove(tempDir);
|
||||
|
||||
let client = getGitHubClient(token);
|
||||
await client.repos
|
||||
.delete({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
.catch(errorHandler);
|
||||
|
||||
console.log('Deleting forked repository', `${forkOwner}/${repo}`);
|
||||
client = getGitHubClient(forkToken);
|
||||
await client.repos
|
||||
.delete({
|
||||
owner: forkOwner,
|
||||
repo,
|
||||
})
|
||||
.catch(errorHandler);
|
||||
}
|
||||
|
||||
async function resetOriginRepo({ owner, repo, tempDir }) {
|
||||
console.log('Resetting origin repo:', `${owner}/repo`);
|
||||
const { token } = getEnvs();
|
||||
const client = getGitHubClient(token);
|
||||
|
||||
const { data: prs } = await client.pulls.list({
|
||||
repo,
|
||||
owner,
|
||||
state: 'open',
|
||||
});
|
||||
const numbers = prs.map(pr => pr.number);
|
||||
console.log('Closing prs:', numbers);
|
||||
await Promise.all(
|
||||
numbers.map(pull_number =>
|
||||
client.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const { data: branches } = await client.repos.listBranches({ owner, repo });
|
||||
const refs = branches.filter(b => b.name !== 'master').map(b => `heads/${b.name}`);
|
||||
|
||||
console.log('Deleting refs', refs);
|
||||
await Promise.all(
|
||||
refs.map(ref =>
|
||||
client.git.deleteRef({
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
console.log('Resetting master');
|
||||
const git = simpleGit(tempDir).env({ ...process.env, GIT_SSH_COMMAND });
|
||||
await git.push(['--force', 'origin', 'master']);
|
||||
console.log('Done resetting origin repo:', `${owner}/repo`);
|
||||
}
|
||||
|
||||
async function resetForkedRepo({ repo }) {
|
||||
const { forkToken, forkOwner } = getEnvs();
|
||||
const client = getGitHubClient(forkToken);
|
||||
|
||||
const { data: repos } = await client.repos.list();
|
||||
if (repos.some(r => r.name === repo)) {
|
||||
console.log('Resetting forked repo:', `${forkOwner}/${repo}`);
|
||||
const { data: branches } = await client.repos.listBranches({ owner: forkOwner, repo });
|
||||
const refs = branches.filter(b => b.name !== 'master').map(b => `heads/${b.name}`);
|
||||
|
||||
console.log('Deleting refs', refs);
|
||||
await Promise.all(
|
||||
refs.map(ref =>
|
||||
client.git.deleteRef({
|
||||
owner: forkOwner,
|
||||
repo,
|
||||
ref,
|
||||
}),
|
||||
),
|
||||
);
|
||||
console.log('Done resetting forked repo:', `${forkOwner}/repo`);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetRepositories({ owner, repo, tempDir }) {
|
||||
await resetOriginRepo({ owner, repo, tempDir });
|
||||
await resetForkedRepo({ repo });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
prepareTestGitHubRepo,
|
||||
deleteRepositories,
|
||||
getUser,
|
||||
getForkUser,
|
||||
resetRepositories,
|
||||
};
|
@ -10,8 +10,106 @@
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
require('dotenv').config();
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const {
|
||||
prepareTestGitHubRepo,
|
||||
deleteRepositories,
|
||||
getUser,
|
||||
getForkUser,
|
||||
resetRepositories,
|
||||
} = require('./github');
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
const devTestDirectory = path.join(__dirname, '..', '..', 'dev-test');
|
||||
const backendsDirectory = path.join(devTestDirectory, 'backends');
|
||||
|
||||
async function copyBackendFiles(backend) {
|
||||
for (let file of ['config.yml', 'index.html']) {
|
||||
await fs.copyFile(
|
||||
path.join(backendsDirectory, backend, file),
|
||||
path.join(devTestDirectory, file),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateConfig(configModifier) {
|
||||
const configFile = path.join(devTestDirectory, 'config.yml');
|
||||
const configContent = await fs.readFile(configFile);
|
||||
const config = yaml.safeLoad(configContent);
|
||||
await configModifier(config);
|
||||
await fs.writeFileSync(configFile, yaml.safeDump(config));
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setupGitHub() {
|
||||
const [user, forkUser, repoData] = await Promise.all([
|
||||
getUser(),
|
||||
getForkUser(),
|
||||
prepareTestGitHubRepo(),
|
||||
]);
|
||||
|
||||
await updateConfig(config => {
|
||||
config.backend.repo = `${repoData.owner}/${repoData.repo}`;
|
||||
});
|
||||
|
||||
return { ...repoData, user, forkUser };
|
||||
}
|
||||
|
||||
async function teardownGitHub(taskData) {
|
||||
await deleteRepositories(taskData);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function teardownBackendTest(taskData) {
|
||||
await resetRepositories(taskData);
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = async on => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
on('task', {
|
||||
async setupBackend({ backend }) {
|
||||
console.log('Preparing environment for backend', backend);
|
||||
await copyBackendFiles(backend);
|
||||
|
||||
if (backend === 'github') {
|
||||
return await setupGitHub();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async teardownBackend(taskData) {
|
||||
const { backend } = taskData;
|
||||
console.log('Tearing down backend', backend);
|
||||
if (backend === 'github') {
|
||||
return await teardownGitHub(taskData);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async teardownBackendTest(taskData) {
|
||||
const { backend } = taskData;
|
||||
console.log('Tearing down single test for backend', backend);
|
||||
if (backend === 'github') {
|
||||
return await teardownBackendTest(taskData);
|
||||
}
|
||||
},
|
||||
async updateBackendOptions({ backend, options }) {
|
||||
console.log('Updating backend', backend, 'with options', options);
|
||||
if (backend === 'github') {
|
||||
return await updateConfig(config => {
|
||||
config.backend = { ...config.backend, ...options };
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
async restoreDefaults() {
|
||||
console.log('Restoring defaults');
|
||||
await copyBackendFiles('test');
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
27
cypress/utils/constants.js
Normal file
27
cypress/utils/constants.js
Normal file
@ -0,0 +1,27 @@
|
||||
const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
|
||||
const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
|
||||
const setting1 = { limit: 10, author: 'John Doe' };
|
||||
const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
|
||||
const notifications = {
|
||||
saved: 'Entry saved',
|
||||
published: 'Entry published',
|
||||
updated: 'Entry status updated',
|
||||
deletedUnpublished: 'Unpublished changes deleted',
|
||||
error: {
|
||||
missingField: "Oops, you've missed a required field. Please complete before saving.",
|
||||
},
|
||||
validation: {
|
||||
range: {
|
||||
fieldLabel: 'Number of posts on frontpage',
|
||||
message: 'Number of posts on frontpage must be between 1 and 10.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
workflowStatus,
|
||||
editorStatus,
|
||||
setting1,
|
||||
setting2,
|
||||
notifications,
|
||||
};
|
292
cypress/utils/steps.js
Normal file
292
cypress/utils/steps.js
Normal file
@ -0,0 +1,292 @@
|
||||
const { notifications, workflowStatus } = require('./constants');
|
||||
|
||||
function login(user) {
|
||||
cy.viewport(1200, 1200);
|
||||
if (user) {
|
||||
cy.visit('/', {
|
||||
onBeforeLoad: () => {
|
||||
window.localStorage.setItem('netlify-cms-user', JSON.stringify(user));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
cy.visit('/');
|
||||
cy.contains('button', 'Login').click();
|
||||
}
|
||||
}
|
||||
|
||||
function assertNotification(message) {
|
||||
if (Array.isArray(message)) {
|
||||
console.log(message);
|
||||
const messages = message.reverse();
|
||||
cy.get('.notif__container div')
|
||||
.should('have.length.of', messages.length)
|
||||
.each((el, idx) => {
|
||||
cy.wrap(el)
|
||||
.contains(messages[idx])
|
||||
.invoke('hide');
|
||||
});
|
||||
} else {
|
||||
cy.get('.notif__container').within(() => {
|
||||
cy.contains(message).invoke('hide');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function exitEditor() {
|
||||
cy.contains('a[href^="#/collections/"]', 'Writing in').click();
|
||||
}
|
||||
|
||||
function goToWorkflow() {
|
||||
cy.contains('a', 'Workflow').click();
|
||||
}
|
||||
|
||||
function goToCollections() {
|
||||
cy.contains('a', 'Content').click();
|
||||
}
|
||||
|
||||
function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) {
|
||||
cy.contains('h2', fromColumnHeading)
|
||||
.parent()
|
||||
.contains('a', title)
|
||||
.trigger('dragstart', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
cy.contains('h2', toColumnHeading)
|
||||
.parent()
|
||||
.trigger('drop', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
assertNotification(notifications.updated);
|
||||
}
|
||||
|
||||
function publishWorkflowEntry({ title }) {
|
||||
cy.contains('h2', workflowStatus.ready)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('a', title)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('button', 'Publish new entry').click({ force: true });
|
||||
});
|
||||
});
|
||||
assertNotification(notifications.published);
|
||||
}
|
||||
|
||||
function deleteWorkflowEntry({ title }) {
|
||||
cy.contains('a', title)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.contains('button', 'Delete new entry').click({ force: true });
|
||||
});
|
||||
|
||||
assertNotification(notifications.deletedUnpublished);
|
||||
}
|
||||
|
||||
function assertWorkflowStatusInEditor(status) {
|
||||
cy.contains('[role="button"]', 'Set status').as('setStatusButton');
|
||||
cy.get('@setStatusButton')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('@setStatusButton').click();
|
||||
cy.contains('[role="menuitem"] span', status)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('svg');
|
||||
});
|
||||
cy.get('@setStatusButton').click();
|
||||
});
|
||||
}
|
||||
|
||||
function assertPublishedEntry(entry) {
|
||||
if (Array.isArray(entry)) {
|
||||
const entries = entry.reverse();
|
||||
cy.get('a h2').then(els => {
|
||||
cy.wrap(els.slice(0, entries.length)).each((el, idx) => {
|
||||
cy.wrap(el).contains(entries[idx].title);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cy.get('a h2')
|
||||
.first()
|
||||
.contains(entry.title);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteEntryInEditor() {
|
||||
cy.contains('button', 'Delete').click();
|
||||
assertNotification(notifications.deletedUnpublished);
|
||||
}
|
||||
|
||||
function assertOnCollectionsPage() {
|
||||
cy.url().should('contain', '/#/collections/posts');
|
||||
cy.contains('h2', 'Collections');
|
||||
}
|
||||
|
||||
function assertEntryDeleted(entry) {
|
||||
if (Array.isArray(entry)) {
|
||||
const titles = entry.map(e => e.title);
|
||||
cy.get('a h2').each(el => {
|
||||
expect(titles).not.to.include(el.text());
|
||||
});
|
||||
} else {
|
||||
cy.get('a h2').each(el => {
|
||||
expect(entry.title).not.to.equal(el.text());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function assertWorkflowStatus({ title }, status) {
|
||||
cy.contains('h2', status)
|
||||
.parent()
|
||||
.contains('a', title);
|
||||
}
|
||||
|
||||
function updateWorkflowStatusInEditor(newStatus) {
|
||||
cy.contains('[role="button"]', 'Set status').as('setStatusButton');
|
||||
cy.get('@setStatusButton')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('@setStatusButton').click();
|
||||
cy.contains('[role="menuitem"] span', newStatus).click();
|
||||
});
|
||||
assertNotification(notifications.updated);
|
||||
}
|
||||
|
||||
function populateEntry(entry) {
|
||||
const keys = Object.keys(entry);
|
||||
for (let key of keys) {
|
||||
const value = entry[key];
|
||||
if (key === 'body') {
|
||||
cy.get('[data-slate-editor]')
|
||||
.click()
|
||||
.clear()
|
||||
.type(value);
|
||||
} else {
|
||||
cy.get(`[id^="${key}-field"]`)
|
||||
.clear()
|
||||
.type(value);
|
||||
}
|
||||
}
|
||||
|
||||
cy.get('input')
|
||||
.first()
|
||||
.click();
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function createPost(entry) {
|
||||
cy.contains('a', 'New Post').click();
|
||||
populateEntry(entry);
|
||||
}
|
||||
|
||||
function createPostAndExit(entry) {
|
||||
createPost(entry);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function updateExistingPostAndExit(fromEntry, toEntry) {
|
||||
goToWorkflow();
|
||||
cy.contains('h2', fromEntry.title)
|
||||
.parent()
|
||||
.click({ force: true });
|
||||
populateEntry(toEntry);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
cy.contains('h2', toEntry.title);
|
||||
}
|
||||
|
||||
function validateObjectFields({ limit, author }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||
cy.get('input[type=number]').type(limit);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
cy.contains('label', 'Default Author').click();
|
||||
cy.focused().type(author);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function validateNestedObjectFields({ limit, author }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||
cy.contains('label', 'Default Author').click();
|
||||
cy.focused().type(author);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
cy.get('input[type=number]').type(limit + 1);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertFieldValidationError(notifications.validation.range);
|
||||
cy.get('input[type=number]')
|
||||
.clear()
|
||||
.type(-1);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertFieldValidationError(notifications.validation.range);
|
||||
cy.get('input[type=number]')
|
||||
.clear()
|
||||
.type(limit);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
}
|
||||
|
||||
function validateListFields({ name, description }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/authors"]').click();
|
||||
cy.contains('button', 'Add').click();
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.error.missingField);
|
||||
cy.get('input')
|
||||
.eq(2)
|
||||
.type(name);
|
||||
cy.get('[data-slate-editor]')
|
||||
.eq(2)
|
||||
.type(description);
|
||||
cy.contains('button', 'Save').click();
|
||||
}
|
||||
|
||||
function validateObjectFieldsAndExit(setting) {
|
||||
validateObjectFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateNestedObjectFieldsAndExit(setting) {
|
||||
validateNestedObjectFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function validateListFieldsAndExit(setting) {
|
||||
validateListFields(setting);
|
||||
exitEditor();
|
||||
}
|
||||
|
||||
function assertFieldValidationError({ message, fieldLabel }) {
|
||||
cy.contains('label', fieldLabel)
|
||||
.siblings('ul[class*=ControlErrorsList]')
|
||||
.contains(message);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
createPost,
|
||||
createPostAndExit,
|
||||
updateExistingPostAndExit,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
goToCollections,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
deleteWorkflowEntry,
|
||||
assertWorkflowStatusInEditor,
|
||||
assertPublishedEntry,
|
||||
deleteEntryInEditor,
|
||||
assertOnCollectionsPage,
|
||||
assertEntryDeleted,
|
||||
assertWorkflowStatus,
|
||||
updateWorkflowStatusInEditor,
|
||||
validateObjectFieldsAndExit,
|
||||
validateNestedObjectFieldsAndExit,
|
||||
validateListFieldsAndExit,
|
||||
};
|
65
dev-test/backends/github/config.yml
Normal file
65
dev-test/backends/github/config.yml
Normal file
@ -0,0 +1,65 @@
|
||||
backend:
|
||||
name: github
|
||||
branch: master
|
||||
repo: owner/repo
|
||||
|
||||
publish_mode: editorial_workflow
|
||||
media_folder: static/media
|
||||
public_folder: /media
|
||||
collections:
|
||||
- name: posts
|
||||
label: Posts
|
||||
label_singular: 'Post'
|
||||
folder: content/posts
|
||||
create: true
|
||||
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
||||
preview_path: 'posts/{{slug}}/index.html'
|
||||
fields:
|
||||
- label: Template
|
||||
name: template
|
||||
widget: hidden
|
||||
default: post
|
||||
- label: Title
|
||||
name: title
|
||||
widget: string
|
||||
- label: Draft
|
||||
name: draft
|
||||
widget: boolean
|
||||
default: true
|
||||
- label: Publish Date
|
||||
name: date
|
||||
widget: datetime
|
||||
- label: Description
|
||||
name: description
|
||||
widget: text
|
||||
- label: Category
|
||||
name: category
|
||||
widget: string
|
||||
- label: Body
|
||||
name: body
|
||||
widget: markdown
|
||||
- label: Tags
|
||||
name: tags
|
||||
widget: list
|
||||
- name: pages
|
||||
label: Pages
|
||||
label_singular: 'Page'
|
||||
folder: content/pages
|
||||
create: true
|
||||
slug: '{{slug}}'
|
||||
preview_path: 'pages/{{slug}}/index.html'
|
||||
fields:
|
||||
- label: Template
|
||||
name: template
|
||||
widget: hidden
|
||||
default: page
|
||||
- label: Title
|
||||
name: title
|
||||
widget: string
|
||||
- label: Draft
|
||||
name: draft
|
||||
widget: boolean
|
||||
default: true
|
||||
- label: Body
|
||||
name: body
|
||||
widget: markdown
|
41
dev-test/backends/github/index.html
Normal file
41
dev-test/backends/github/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Netlify CMS Development Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="dist/netlify-cms.js"></script>
|
||||
<script>
|
||||
var PostPreview = createClass({
|
||||
render: function() {
|
||||
var entry = this.props.entry;
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
h('div', { className: 'cover' }, h('h1', {}, entry.getIn(['data', 'title']))),
|
||||
h('p', {}, h('small', {}, 'Written ' + entry.getIn(['data', 'date']))),
|
||||
h('div', { className: 'text' }, this.props.widgetFor('body')),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
var PagePreview = createClass({
|
||||
render: function() {
|
||||
var entry = this.props.entry;
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
h('div', { className: 'cover' }, h('h1', {}, entry.getIn(['data', 'title']))),
|
||||
h('p', {}, h('small', {}, 'Written ' + entry.getIn(['data', 'date']))),
|
||||
h('div', { className: 'text' }, this.props.widgetFor('body')),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
CMS.registerPreviewTemplate('posts', PostPreview);
|
||||
CMS.registerPreviewTemplate('pages', PagePreview);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
231
dev-test/backends/test/config.yml
Normal file
231
dev-test/backends/test/config.yml
Normal file
@ -0,0 +1,231 @@
|
||||
backend:
|
||||
name: test-repo
|
||||
|
||||
site_url: https://example.com
|
||||
|
||||
publish_mode: editorial_workflow
|
||||
media_folder: assets/uploads
|
||||
|
||||
collections: # A list of collections the CMS should be able to edit
|
||||
- name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
|
||||
label: 'Posts' # Used in the UI
|
||||
label_singular: 'Post' # Used in the UI, ie: "New Post"
|
||||
description: >
|
||||
The description is a great place for tone setting, high level information, and editing
|
||||
guidelines that are specific to a collection.
|
||||
folder: '_posts'
|
||||
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
||||
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
|
||||
create: true # Allow users to create new documents in this collection
|
||||
fields: # The fields each document in this collection have
|
||||
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
|
||||
- {
|
||||
label: 'Publish Date',
|
||||
name: 'date',
|
||||
widget: 'datetime',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
timeFormat: 'HH:mm',
|
||||
format: 'YYYY-MM-DD HH:mm',
|
||||
}
|
||||
- label: 'Cover Image'
|
||||
name: 'image'
|
||||
widget: 'image'
|
||||
required: false
|
||||
tagname: ''
|
||||
|
||||
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
|
||||
meta:
|
||||
- { label: 'SEO Description', name: 'description', widget: 'text' }
|
||||
|
||||
- name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit
|
||||
label: 'FAQ' # Used in the UI
|
||||
folder: '_faqs'
|
||||
create: true # Allow users to create new documents in this collection
|
||||
fields: # The fields each document in this collection have
|
||||
- { label: 'Question', name: 'title', widget: 'string', tagname: 'h1' }
|
||||
- { label: 'Answer', name: 'body', widget: 'markdown' }
|
||||
|
||||
- name: 'settings'
|
||||
label: 'Settings'
|
||||
delete: false # Prevent users from deleting documents in this collection
|
||||
editor:
|
||||
preview: false
|
||||
files:
|
||||
- name: 'general'
|
||||
label: 'Site Settings'
|
||||
file: '_data/settings.json'
|
||||
description: 'General Site Settings'
|
||||
fields:
|
||||
- { label: 'Global title', name: 'site_title', widget: 'string' }
|
||||
- label: 'Post Settings'
|
||||
name: posts
|
||||
widget: 'object'
|
||||
fields:
|
||||
- {
|
||||
label: 'Number of posts on frontpage',
|
||||
name: front_limit,
|
||||
widget: number,
|
||||
min: 1,
|
||||
max: 10,
|
||||
}
|
||||
- { label: 'Default Author', name: author, widget: string }
|
||||
- { label: 'Default Thumbnail', name: thumb, widget: image, class: 'thumb', required: false }
|
||||
|
||||
- name: 'authors'
|
||||
label: 'Authors'
|
||||
file: '_data/authors.yml'
|
||||
description: 'Author descriptions'
|
||||
fields:
|
||||
- name: authors
|
||||
label: Authors
|
||||
label_singular: 'Author'
|
||||
widget: list
|
||||
fields:
|
||||
- { label: 'Name', name: 'name', widget: 'string', hint: 'First and Last' }
|
||||
- { label: 'Description', name: 'description', widget: 'markdown' }
|
||||
|
||||
- name: 'kitchenSink' # all the things in one entry, for documentation and quick testing
|
||||
label: 'Kitchen Sink'
|
||||
folder: '_sink'
|
||||
create: true
|
||||
fields:
|
||||
- label: 'Related Post'
|
||||
name: 'post'
|
||||
widget: 'relationKitchenSinkPost'
|
||||
collection: 'posts'
|
||||
displayFields: ['title', 'date']
|
||||
searchFields: ['title', 'body']
|
||||
valueField: 'title'
|
||||
- { label: 'Title', name: 'title', widget: 'string' }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true }
|
||||
- { label: 'Map', name: 'map', widget: 'map' }
|
||||
- { label: 'Text', name: 'text', widget: 'text', hint: 'Plain text, not markdown' }
|
||||
- { label: 'Number', name: 'number', widget: 'number', hint: 'To infinity and beyond!' }
|
||||
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
|
||||
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
|
||||
- { label: 'Date', name: 'date', widget: 'date' }
|
||||
- { label: 'Image', name: 'image', widget: 'image' }
|
||||
- { label: 'File', name: 'file', widget: 'file' }
|
||||
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
|
||||
- {
|
||||
label: 'Select multiple',
|
||||
name: 'select_multiple',
|
||||
widget: 'select',
|
||||
options: ['a', 'b', 'c'],
|
||||
multiple: true,
|
||||
}
|
||||
- { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
|
||||
- label: 'Object'
|
||||
name: 'object'
|
||||
widget: 'object'
|
||||
fields:
|
||||
- label: 'Related Post'
|
||||
name: 'post'
|
||||
widget: 'relationKitchenSinkPost'
|
||||
collection: 'posts'
|
||||
searchFields: ['title', 'body']
|
||||
valueField: 'title'
|
||||
- { label: 'String', name: 'string', widget: 'string' }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false }
|
||||
- { label: 'Text', name: 'text', widget: 'text' }
|
||||
- { label: 'Number', name: 'number', widget: 'number' }
|
||||
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
|
||||
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
|
||||
- { label: 'Date', name: 'date', widget: 'date' }
|
||||
- { label: 'Image', name: 'image', widget: 'image' }
|
||||
- { label: 'File', name: 'file', widget: 'file' }
|
||||
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
|
||||
- label: 'List'
|
||||
name: 'list'
|
||||
widget: 'list'
|
||||
fields:
|
||||
- { label: 'String', name: 'string', widget: 'string' }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
|
||||
- { label: 'Text', name: 'text', widget: 'text' }
|
||||
- { label: 'Number', name: 'number', widget: 'number' }
|
||||
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
|
||||
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
|
||||
- { label: 'Date', name: 'date', widget: 'date' }
|
||||
- { label: 'Image', name: 'image', widget: 'image' }
|
||||
- { label: 'File', name: 'file', widget: 'file' }
|
||||
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
|
||||
- label: 'Object'
|
||||
name: 'object'
|
||||
widget: 'object'
|
||||
fields:
|
||||
- { label: 'String', name: 'string', widget: 'string' }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
|
||||
- { label: 'Text', name: 'text', widget: 'text' }
|
||||
- { label: 'Number', name: 'number', widget: 'number' }
|
||||
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
|
||||
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
|
||||
- { label: 'Date', name: 'date', widget: 'date' }
|
||||
- { label: 'Image', name: 'image', widget: 'image' }
|
||||
- { label: 'File', name: 'file', widget: 'file' }
|
||||
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
|
||||
- label: 'List'
|
||||
name: 'list'
|
||||
widget: 'list'
|
||||
fields:
|
||||
- label: 'Related Post'
|
||||
name: 'post'
|
||||
widget: 'relationKitchenSinkPost'
|
||||
collection: 'posts'
|
||||
searchFields: ['title', 'body']
|
||||
valueField: 'title'
|
||||
- { label: 'String', name: 'string', widget: 'string' }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
|
||||
- { label: 'Text', name: 'text', widget: 'text' }
|
||||
- { label: 'Number', name: 'number', widget: 'number' }
|
||||
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
|
||||
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
|
||||
- { label: 'Date', name: 'date', widget: 'date' }
|
||||
- { label: 'Image', name: 'image', widget: 'image' }
|
||||
- { label: 'File', name: 'file', widget: 'file' }
|
||||
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
|
||||
- { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
|
||||
- label: 'Object'
|
||||
name: 'object'
|
||||
widget: 'object'
|
||||
fields:
|
||||
- { label: 'String', name: 'string', widget: 'string' }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
|
||||
- { label: 'Text', name: 'text', widget: 'text' }
|
||||
- { label: 'Number', name: 'number', widget: 'number' }
|
||||
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
|
||||
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
|
||||
- { label: 'Date', name: 'date', widget: 'date' }
|
||||
- { label: 'Image', name: 'image', widget: 'image' }
|
||||
- { label: 'File', name: 'file', widget: 'file' }
|
||||
- {
|
||||
label: 'Select',
|
||||
name: 'select',
|
||||
widget: 'select',
|
||||
options: ['a', 'b', 'c'],
|
||||
}
|
||||
- label: 'Typed List'
|
||||
name: 'typed_list'
|
||||
widget: 'list'
|
||||
types:
|
||||
- label: 'Type 1 Object'
|
||||
name: 'type_1_object'
|
||||
widget: 'object'
|
||||
fields:
|
||||
- { label: 'String', name: 'string', widget: 'string' }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
|
||||
- { label: 'Text', name: 'text', widget: 'text' }
|
||||
- label: 'Type 2 Object'
|
||||
name: 'type_2_object'
|
||||
widget: 'object'
|
||||
fields:
|
||||
- { label: 'Number', name: 'number', widget: 'number' }
|
||||
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
|
||||
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
|
||||
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
|
||||
- label: 'Type 3 Object'
|
||||
name: 'type_3_object'
|
||||
widget: 'object'
|
||||
fields:
|
||||
- { label: 'Date', name: 'date', widget: 'date' }
|
||||
- { label: 'Image', name: 'image', widget: 'image' }
|
||||
- { label: 'File', name: 'file', widget: 'file' }
|
208
dev-test/backends/test/index.html
Normal file
208
dev-test/backends/test/index.html
Normal file
File diff suppressed because one or more lines are too long
@ -72,6 +72,7 @@
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.3.4",
|
||||
"@babel/preset-env": "^7.3.4",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@octokit/rest": "^16.28.7",
|
||||
"all-contributors-cli": "^6.0.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
@ -89,17 +90,21 @@
|
||||
"copy-webpack-plugin": "^5.0.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.0.0",
|
||||
"cypress": "^3.1.5",
|
||||
"cypress": "^3.4.1",
|
||||
"dom-testing-library": "^4.0.0",
|
||||
"dotenv": "^8.0.0",
|
||||
"eslint": "^5.15.1",
|
||||
"eslint-plugin-cypress": "^2.6.0",
|
||||
"eslint-plugin-emotion": "^10.0.7",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"http-server": "^0.11.1",
|
||||
"jest": "^24.5.0",
|
||||
"jest-cli": "^24.5.0",
|
||||
"jest-dom": "^3.1.3",
|
||||
"jest-emotion": "^10.0.9",
|
||||
"js-yaml": "^3.13.1",
|
||||
"ncp": "^2.0.0",
|
||||
"nock": "^10.0.4",
|
||||
"node-fetch": "^2.3.0",
|
||||
@ -108,6 +113,7 @@
|
||||
"react-test-renderer": "^16.8.4",
|
||||
"react-testing-library": "^7.0.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"simple-git": "^1.124.0",
|
||||
"start-server-and-test": "^1.7.11",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
|
@ -17,10 +17,17 @@
|
||||
"scripts": {
|
||||
"develop": "yarn build:esm --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore **/__tests__ --root-mode upward"
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore **/__tests__ --root-mode upward",
|
||||
"createFragmentTypes": "node scripts/createFragmentTypes.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-cache-inmemory": "^1.6.2",
|
||||
"apollo-client": "^2.6.3",
|
||||
"apollo-link-context": "^1.0.18",
|
||||
"apollo-link-http": "^1.5.15",
|
||||
"common-tags": "^1.8.0",
|
||||
"graphql": "^14.4.2",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"js-base64": "^2.5.1",
|
||||
"semaphore": "^1.1.0"
|
||||
},
|
||||
|
@ -0,0 +1,48 @@
|
||||
const fetch = require('node-fetch');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const API_HOST = process.env.GITHUB_HOST || 'https://api.github.com';
|
||||
const API_TOKEN = process.env.GITHUB_API_TOKEN;
|
||||
|
||||
if (!API_TOKEN) {
|
||||
throw new Error('Missing environment variable GITHUB_API_TOKEN');
|
||||
}
|
||||
|
||||
fetch(`${API_HOST}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `bearer ${API_TOKEN}` },
|
||||
body: JSON.stringify({
|
||||
variables: {},
|
||||
query: `
|
||||
{
|
||||
__schema {
|
||||
types {
|
||||
kind
|
||||
name
|
||||
possibleTypes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
})
|
||||
.then(result => result.json())
|
||||
.then(result => {
|
||||
// here we're filtering out any type information unrelated to unions or interfaces
|
||||
const filteredData = result.data.__schema.types.filter(type => type.possibleTypes !== null);
|
||||
result.data.__schema.types = filteredData;
|
||||
fs.writeFile(
|
||||
path.join(__dirname, '..', 'src', 'fragmentTypes.js'),
|
||||
`module.exports = ${JSON.stringify(result.data)}`,
|
||||
err => {
|
||||
if (err) {
|
||||
console.error('Error writing fragmentTypes file', err);
|
||||
} else {
|
||||
console.log('Fragment types successfully extracted!');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
@ -3,7 +3,7 @@ import semaphore from 'semaphore';
|
||||
import { find, flow, get, hasIn, initial, last, partial, result, uniq } from 'lodash';
|
||||
import { map } from 'lodash/fp';
|
||||
import {
|
||||
getPaginatedRequestIterator,
|
||||
getAllResponses,
|
||||
APIError,
|
||||
EditorialWorkflowError,
|
||||
filterPromisesWith,
|
||||
@ -21,15 +21,19 @@ export default class API {
|
||||
this.api_root = config.api_root || 'https://api.github.com';
|
||||
this.token = config.token || false;
|
||||
this.branch = config.branch || 'master';
|
||||
this.originRepo = config.originRepo;
|
||||
this.useOpenAuthoring = config.useOpenAuthoring;
|
||||
this.repo = config.repo || '';
|
||||
this.originRepo = config.originRepo || this.repo;
|
||||
this.repoURL = `/repos/${this.repo}`;
|
||||
this.originRepoURL = this.originRepo && `/repos/${this.originRepo}`;
|
||||
// when not in 'useOpenAuthoring' mode originRepoURL === repoURL
|
||||
this.originRepoURL = `/repos/${this.originRepo}`;
|
||||
this.merge_method = config.squash_merges ? 'squash' : 'merge';
|
||||
this.initialWorkflowStatus = config.initialWorkflowStatus;
|
||||
}
|
||||
|
||||
static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Netlify CMS';
|
||||
static DEFAULT_PR_BODY = 'Automatically generated by Netlify CMS';
|
||||
|
||||
user() {
|
||||
if (!this._userPromise) {
|
||||
this._userPromise = this.request('/user');
|
||||
@ -113,13 +117,10 @@ export default class API {
|
||||
}
|
||||
|
||||
async requestAllPages(url, options = {}) {
|
||||
const headers = this.requestHeaders(options.headers || {});
|
||||
const processedURL = this.urlFor(url, options);
|
||||
const pagesIterator = getPaginatedRequestIterator(processedURL, options);
|
||||
const pagesToParse = [];
|
||||
for await (const page of pagesIterator) {
|
||||
pagesToParse.push(this.parseResponse(page));
|
||||
}
|
||||
const pages = await Promise.all(pagesToParse);
|
||||
const allResponses = await getAllResponses(processedURL, { ...options, headers });
|
||||
const pages = await Promise.all(allResponses.map(res => this.parseResponse(res)));
|
||||
return [].concat(...pages);
|
||||
}
|
||||
|
||||
@ -224,34 +225,46 @@ export default class API {
|
||||
cache: 'no-store',
|
||||
};
|
||||
|
||||
const errorHandler = err => {
|
||||
if (err.message === 'Not Found') {
|
||||
console.log(
|
||||
'%c %s does not have metadata',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
key,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
};
|
||||
|
||||
if (!this.useOpenAuthoring) {
|
||||
return this.request(`${this.repoURL}/contents/${key}.json`, metadataRequestOptions)
|
||||
.then(response => JSON.parse(response))
|
||||
.catch(err => {
|
||||
if (err.message === 'Not Found') {
|
||||
console.log(
|
||||
'%c %s does not have metadata',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
key,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
.catch(errorHandler);
|
||||
}
|
||||
|
||||
const [user, repo] = key.split('/');
|
||||
return this.request(`/repos/${user}/${repo}/contents/${key}.json`, metadataRequestOptions)
|
||||
.then(response => JSON.parse(response))
|
||||
.catch(err => {
|
||||
if (err.message === 'Not Found') {
|
||||
console.log(
|
||||
'%c %s does not have metadata',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
key,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
.catch(errorHandler);
|
||||
});
|
||||
}
|
||||
|
||||
retrieveContent(path, branch, repoURL) {
|
||||
return this.request(`${repoURL}/contents/${path}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
params: { ref: branch },
|
||||
cache: 'no-store',
|
||||
}).catch(error => {
|
||||
if (hasIn(error, 'message.errors') && find(error.message.errors, { code: 'too_large' })) {
|
||||
const dir = path
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.join('/');
|
||||
return this.listFiles(dir, { repoURL, branch })
|
||||
.then(files => files.find(file => file.path === path))
|
||||
.then(file => this.getBlob(file.sha, { repoURL }));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@ -259,34 +272,23 @@ export default class API {
|
||||
if (sha) {
|
||||
return this.getBlob(sha);
|
||||
} else {
|
||||
return this.request(`${repoURL}/contents/${path}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
params: { ref: branch },
|
||||
cache: 'no-store',
|
||||
}).catch(error => {
|
||||
if (hasIn(error, 'message.errors') && find(error.message.errors, { code: 'too_large' })) {
|
||||
const dir = path
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.join('/');
|
||||
return this.listFiles(dir, { repoURL, branch })
|
||||
.then(files => files.find(file => file.path === path))
|
||||
.then(file => this.getBlob(file.sha, { repoURL }));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return this.retrieveContent(path, branch, repoURL);
|
||||
}
|
||||
}
|
||||
|
||||
retrieveBlob(sha, repoURL) {
|
||||
return this.request(`${repoURL}/git/blobs/${sha}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
});
|
||||
}
|
||||
|
||||
getBlob(sha, { repoURL = this.repoURL } = {}) {
|
||||
return localForage.getItem(`gh.${sha}`).then(cached => {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return this.request(`${repoURL}/git/blobs/${sha}`, {
|
||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||
}).then(result => {
|
||||
return this.retrieveBlob(sha, repoURL).then(result => {
|
||||
localForage.setItem(`gh.${sha}`, result);
|
||||
return result;
|
||||
});
|
||||
@ -335,7 +337,7 @@ export default class API {
|
||||
isUnpublishedEntryModification(path, branch) {
|
||||
return this.readFile(path, null, {
|
||||
branch,
|
||||
repoURL: this.useOpenAuthoring ? this.originRepoURL : this.repoURL,
|
||||
repoURL: this.originRepoURL,
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(err => {
|
||||
@ -385,7 +387,7 @@ export default class API {
|
||||
// closed or not and update the status accordingly.
|
||||
if (prMetadata) {
|
||||
const { number: prNumber } = prMetadata;
|
||||
const originPRInfo = await this.request(`${this.originRepoURL}/pulls/${prNumber}`);
|
||||
const originPRInfo = await this.getPullRequest(prNumber);
|
||||
const { state: currentState, merged_at: mergedAt } = originPRInfo;
|
||||
if (currentState === 'closed' && mergedAt) {
|
||||
// The PR has been merged; delete the unpublished entry
|
||||
@ -453,9 +455,8 @@ export default class API {
|
||||
* concept of entry "status". Useful for things like deploy preview links.
|
||||
*/
|
||||
async getStatuses(sha) {
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
try {
|
||||
const resp = await this.request(`${repoURL}/commits/${sha}/status`);
|
||||
const resp = await this.request(`${this.originRepoURL}/commits/${sha}/status`);
|
||||
return resp.statuses;
|
||||
} catch (err) {
|
||||
if (err && err.message && err.message === 'Ref not found') {
|
||||
@ -517,25 +518,33 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(path, message, options = {}) {
|
||||
if (this.useOpenAuthoring) {
|
||||
return Promise.reject('Cannot delete published entries as an Open Authoring user!');
|
||||
}
|
||||
const branch = options.branch || this.branch;
|
||||
const pathArray = path.split('/');
|
||||
const filename = last(pathArray);
|
||||
const directory = initial(pathArray).join('/');
|
||||
const fileDataPath = encodeURIComponent(directory);
|
||||
const fileDataURL = `${this.repoURL}/git/trees/${branch}:${fileDataPath}`;
|
||||
const fileURL = `${this.repoURL}/contents/${path}`;
|
||||
|
||||
getFileSha(path, branch) {
|
||||
/**
|
||||
* We need to request the tree first to get the SHA. We use extended SHA-1
|
||||
* syntax (<rev>:<path>) to get a blob from a tree without having to recurse
|
||||
* through the tree.
|
||||
*/
|
||||
|
||||
const pathArray = path.split('/');
|
||||
const filename = last(pathArray);
|
||||
const directory = initial(pathArray).join('/');
|
||||
const fileDataPath = encodeURIComponent(directory);
|
||||
const fileDataURL = `${this.repoURL}/git/trees/${branch}:${fileDataPath}`;
|
||||
|
||||
return this.request(fileDataURL, { cache: 'no-store' }).then(resp => {
|
||||
const { sha } = resp.tree.find(file => file.path === filename);
|
||||
return sha;
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(path, message, options = {}) {
|
||||
if (this.useOpenAuthoring) {
|
||||
return Promise.reject('Cannot delete published entries as an Open Authoring user!');
|
||||
}
|
||||
|
||||
const branch = options.branch || this.branch;
|
||||
|
||||
return this.getFileSha(path, branch).then(sha => {
|
||||
const opts = { method: 'DELETE', params: { sha, message, branch } };
|
||||
if (this.commitAuthor) {
|
||||
opts.params.author = {
|
||||
@ -543,10 +552,16 @@ export default class API {
|
||||
date: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
const fileURL = `${this.repoURL}/contents/${path}`;
|
||||
return this.request(fileURL, opts);
|
||||
});
|
||||
}
|
||||
|
||||
async createBranchAndPullRequest(branchName, sha, commitMessage) {
|
||||
await this.createBranch(branchName, sha);
|
||||
return this.createPR(commitMessage, branchName);
|
||||
}
|
||||
|
||||
async editorialWorkflowGit(fileTree, entry, filesList, options) {
|
||||
const contentKey = this.generateContentKey(options.collectionName, entry.slug);
|
||||
const branchName = this.generateBranchName(contentKey);
|
||||
@ -557,10 +572,18 @@ export default class API {
|
||||
const branchData = await this.getBranch();
|
||||
const changeTree = await this.updateTree(branchData.commit.sha, '/', fileTree);
|
||||
const commitResponse = await this.commit(options.commitMessage, changeTree);
|
||||
await this.createBranch(branchName, commitResponse.sha);
|
||||
const pr = this.useOpenAuthoring
|
||||
? undefined
|
||||
: await this.createPR(options.commitMessage, branchName);
|
||||
|
||||
let pr;
|
||||
if (this.useOpenAuthoring) {
|
||||
await this.createBranch(branchName, commitResponse.sha);
|
||||
} else {
|
||||
pr = await this.createBranchAndPullRequest(
|
||||
branchName,
|
||||
commitResponse.sha,
|
||||
options.commitMessage,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await userPromise;
|
||||
return this.storeMetadata(contentKey, {
|
||||
type: 'PR',
|
||||
@ -610,6 +633,9 @@ export default class API {
|
||||
|
||||
if (pr) {
|
||||
return this.rebasePullRequest(pr.number, branchName, contentKey, metadata, commit);
|
||||
} else if (this.useOpenAuthoring) {
|
||||
// if a PR hasn't been created yet for the forked repo, just patch the branch
|
||||
await this.patchBranch(branchName, commit.sha, { force: true });
|
||||
}
|
||||
|
||||
return this.storeMetadata(contentKey, updatedMetadata);
|
||||
@ -629,8 +655,10 @@ export default class API {
|
||||
* Get the published branch and create new commits over it. If the pull
|
||||
* request is up to date, no rebase will occur.
|
||||
*/
|
||||
const baseBranch = await this.getBranch();
|
||||
const commits = await this.getPullRequestCommits(prNumber, head);
|
||||
const [baseBranch, commits] = await Promise.all([
|
||||
this.getBranch(),
|
||||
this.getPullRequestCommits(prNumber, head),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Sometimes the list of commits for a pull request isn't updated
|
||||
@ -731,16 +759,14 @@ export default class API {
|
||||
* Get a pull request by PR number.
|
||||
*/
|
||||
getPullRequest(prNumber) {
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
return this.request(`${repoURL}/pulls/${prNumber} }`);
|
||||
return this.request(`${this.originRepoURL}/pulls/${prNumber} }`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of commits for a given pull request.
|
||||
*/
|
||||
getPullRequestCommits(prNumber) {
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
return this.request(`${repoURL}/pulls/${prNumber}/commits`);
|
||||
return this.request(`${this.originRepoURL}/pulls/${prNumber}/commits`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -779,7 +805,7 @@ export default class API {
|
||||
const { pr: prMetadata } = metadata;
|
||||
if (prMetadata) {
|
||||
const { number: prNumber } = prMetadata;
|
||||
const originPRInfo = await this.request(`${this.originRepoURL}/pulls/${prNumber}`);
|
||||
const originPRInfo = await this.getPullRequest(prNumber);
|
||||
const { state } = originPRInfo;
|
||||
if (state === 'open' && status === 'draft') {
|
||||
await this.closePR(prMetadata);
|
||||
@ -800,7 +826,7 @@ export default class API {
|
||||
|
||||
if (!prMetadata && status === 'pending_review') {
|
||||
const branchName = this.generateBranchName(contentKey);
|
||||
const commitMessage = metadata.commitMessage || 'Automatically generated by Netlify CMS';
|
||||
const commitMessage = metadata.commitMessage || API.DEFAULT_COMMIT_MESSAGE;
|
||||
const { number, head } = await this.createPR(commitMessage, branchName);
|
||||
return this.storeMetadata(contentKey, {
|
||||
...metadata,
|
||||
@ -883,21 +909,23 @@ export default class API {
|
||||
return this.deleteRef('heads', branchName);
|
||||
}
|
||||
|
||||
async createPR(title, head, base = this.branch) {
|
||||
const body = 'Automatically generated by Netlify CMS';
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
async createPR(title, head) {
|
||||
const headReference = this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head;
|
||||
return this.request(`${repoURL}/pulls`, {
|
||||
return this.request(`${this.originRepoURL}/pulls`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, body, head: headReference, base }),
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
body: API.DEFAULT_PR_BODY,
|
||||
head: headReference,
|
||||
base: this.branch,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async openPR(pullRequest) {
|
||||
const { number } = pullRequest;
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
console.log('%c Re-opening PR', 'line-height: 30px;text-align: center;font-weight: bold');
|
||||
return this.request(`${repoURL}/pulls/${number}`, {
|
||||
return this.request(`${this.originRepoURL}/pulls/${number}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
state: 'open',
|
||||
@ -905,11 +933,10 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
closePR(pullrequest) {
|
||||
const prNumber = pullrequest.number;
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
closePR(pullRequest) {
|
||||
const { number } = pullRequest;
|
||||
console.log('%c Deleting PR', 'line-height: 30px;text-align: center;font-weight: bold');
|
||||
return this.request(`${repoURL}/pulls/${prNumber}`, {
|
||||
return this.request(`${this.originRepoURL}/pulls/${number}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
state: 'closed',
|
||||
@ -918,11 +945,9 @@ export default class API {
|
||||
}
|
||||
|
||||
mergePR(pullrequest, objects) {
|
||||
const headSha = pullrequest.head;
|
||||
const prNumber = pullrequest.number;
|
||||
const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
|
||||
const { head: headSha, number } = pullrequest;
|
||||
console.log('%c Merging PR', 'line-height: 30px;text-align: center;font-weight: bold');
|
||||
return this.request(`${repoURL}/pulls/${prNumber}/merge`, {
|
||||
return this.request(`${this.originRepoURL}/pulls/${number}/merge`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
commit_message: 'Automatically generated. Merged on Netlify CMS.',
|
||||
|
627
packages/netlify-cms-backend-github/src/GraphQLAPI.js
Normal file
627
packages/netlify-cms-backend-github/src/GraphQLAPI.js
Normal file
@ -0,0 +1,627 @@
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import {
|
||||
InMemoryCache,
|
||||
defaultDataIdFromObject,
|
||||
IntrospectionFragmentMatcher,
|
||||
} from 'apollo-cache-inmemory';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { setContext } from 'apollo-link-context';
|
||||
import { APIError, EditorialWorkflowError } from 'netlify-cms-lib-util';
|
||||
import introspectionQueryResultData from './fragmentTypes';
|
||||
import API from './API';
|
||||
import * as queries from './queries';
|
||||
import * as mutations from './mutations';
|
||||
|
||||
const NO_CACHE = 'no-cache';
|
||||
const CACHE_FIRST = 'cache-first';
|
||||
|
||||
const TREE_ENTRY_TYPE_TO_MODE = {
|
||||
blob: '100644',
|
||||
tree: '040000',
|
||||
commit: '160000',
|
||||
};
|
||||
|
||||
const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData,
|
||||
});
|
||||
|
||||
export default class GraphQLAPI extends API {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
|
||||
const [repoParts, originRepoParts] = [this.repo.split('/'), this.originRepo.split('/')];
|
||||
this.repo_owner = repoParts[0];
|
||||
this.repo_name = repoParts[1];
|
||||
|
||||
this.origin_repo_owner = originRepoParts[0];
|
||||
this.origin_repo_name = originRepoParts[1];
|
||||
|
||||
this.client = this.getApolloClient();
|
||||
}
|
||||
|
||||
getApolloClient() {
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: this.token ? `token ${this.token}` : '',
|
||||
},
|
||||
};
|
||||
});
|
||||
const httpLink = createHttpLink({ uri: `${this.api_root}/graphql` });
|
||||
return new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache({ fragmentMatcher }),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'ignore',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: NO_CACHE,
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
return this.client.resetStore();
|
||||
}
|
||||
|
||||
async getRepository(owner, name) {
|
||||
const { data } = await this.query({
|
||||
query: queries.repository,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // repository id doesn't change
|
||||
});
|
||||
return data.repository;
|
||||
}
|
||||
|
||||
query(options = {}) {
|
||||
return this.client.query(options).catch(error => {
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
});
|
||||
}
|
||||
|
||||
mutate(options = {}) {
|
||||
return this.client.mutate(options).catch(error => {
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
});
|
||||
}
|
||||
|
||||
async hasWriteAccess() {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
try {
|
||||
const { data } = await this.query({
|
||||
query: queries.repoPermission,
|
||||
variables: { owner, name },
|
||||
fetchPolicy: CACHE_FIRST, // we can assume permission doesn't change often
|
||||
});
|
||||
// https://developer.github.com/v4/enum/repositorypermission/
|
||||
const { viewerPermission } = data.repository;
|
||||
return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(viewerPermission);
|
||||
} catch (error) {
|
||||
console.error('Problem fetching repo data from GitHub');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async user() {
|
||||
const { data } = await this.query({
|
||||
query: queries.user,
|
||||
fetchPolicy: CACHE_FIRST, // we can assume user details don't change often
|
||||
});
|
||||
return data.viewer;
|
||||
}
|
||||
|
||||
async retrieveBlobObject(owner, name, expression, options = {}) {
|
||||
const { data } = await this.query({
|
||||
query: queries.blob,
|
||||
variables: { owner, name, expression },
|
||||
...options,
|
||||
});
|
||||
// https://developer.github.com/v4/object/blob/
|
||||
if (data.repository.object) {
|
||||
const { is_binary, text } = data.repository.object;
|
||||
return { is_null: false, is_binary, text };
|
||||
} else {
|
||||
return { is_null: true };
|
||||
}
|
||||
}
|
||||
|
||||
getOwnerAndNameFromRepoUrl(repoURL) {
|
||||
let { repo_owner: owner, repo_name: name } = this;
|
||||
|
||||
if (repoURL === this.originRepoURL) {
|
||||
({ origin_repo_owner: owner, origin_repo_name: name } = this);
|
||||
}
|
||||
|
||||
return { owner, name };
|
||||
}
|
||||
|
||||
async retrieveContent(path, branch, repoURL) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { is_null, is_binary, text } = await this.retrieveBlobObject(
|
||||
owner,
|
||||
name,
|
||||
`${branch}:${path}`,
|
||||
);
|
||||
if (is_null) {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
} else if (!is_binary) {
|
||||
return text;
|
||||
} else {
|
||||
return super.retrieveContent(path, branch, repoURL);
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveBlob(sha, repoURL) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { is_null, is_binary, text } = await this.retrieveBlobObject(
|
||||
owner,
|
||||
name,
|
||||
sha,
|
||||
{ fetchPolicy: CACHE_FIRST }, // blob sha is derived from file content
|
||||
);
|
||||
|
||||
if (is_null) {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
} else if (!is_binary) {
|
||||
return text;
|
||||
} else {
|
||||
return super.retrieveBlob(sha);
|
||||
}
|
||||
}
|
||||
|
||||
async getStatuses(sha) {
|
||||
const { origin_repo_owner: owner, origin_repo_name: name } = this;
|
||||
const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } });
|
||||
if (data.repository.object) {
|
||||
const { status } = data.repository.object;
|
||||
const { contexts } = status || { contexts: [] };
|
||||
return contexts;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async listFiles(path) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.files,
|
||||
variables: { owner, name, expression: `${this.branch}:${path}` },
|
||||
});
|
||||
|
||||
if (data.repository.object) {
|
||||
const files = data.repository.object.entries.map(e => ({
|
||||
...e,
|
||||
path: `${path}/${e.name}`,
|
||||
download_url: `https://raw.githubusercontent.com/${this.repo}/${this.branch}/${path}/${e.name}`,
|
||||
size: e.blob && e.blob.size,
|
||||
}));
|
||||
return files;
|
||||
} else {
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
}
|
||||
}
|
||||
|
||||
async listUnpublishedBranches() {
|
||||
if (this.useOpenAuthoring) {
|
||||
return super.listUnpublishedBranches();
|
||||
}
|
||||
console.log(
|
||||
'%c Checking for Unpublished entries',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
);
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.unpublishedPrBranches,
|
||||
variables: { owner, name },
|
||||
});
|
||||
const { nodes } = data.repository.refs;
|
||||
if (nodes.length > 0) {
|
||||
const branches = [];
|
||||
nodes.forEach(({ associatedPullRequests }) => {
|
||||
associatedPullRequests.nodes.forEach(({ headRef }) => {
|
||||
branches.push({ ref: `${headRef.prefix}${headRef.name}` });
|
||||
});
|
||||
});
|
||||
return branches;
|
||||
} else {
|
||||
console.log(
|
||||
'%c No Unpublished entries',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
);
|
||||
throw new APIError('Not Found', 404, 'GitHub');
|
||||
}
|
||||
}
|
||||
|
||||
async readUnpublishedBranchFile(contentKey) {
|
||||
// retrieveMetadata(contentKey) rejects in case of no metadata
|
||||
const metaData = await this.retrieveMetadata(contentKey).catch(() => null);
|
||||
if (metaData && metaData.objects && metaData.objects.entry && metaData.objects.entry.path) {
|
||||
const { path } = metaData.objects.entry;
|
||||
const { repo_owner: headOwner, repo_name: headRepoName } = this;
|
||||
const { origin_repo_owner: baseOwner, origin_repo_name: baseRepoName } = this;
|
||||
|
||||
const { data } = await this.query({
|
||||
query: queries.unpublishedBranchFile,
|
||||
variables: {
|
||||
headOwner,
|
||||
headRepoName,
|
||||
headExpression: `${metaData.branch}:${path}`,
|
||||
baseOwner,
|
||||
baseRepoName,
|
||||
baseExpression: `${this.branch}:${path}`,
|
||||
},
|
||||
});
|
||||
if (!data.head.object) {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
const result = {
|
||||
metaData,
|
||||
fileData: data.head.object.text,
|
||||
isModification: !!data.base.object,
|
||||
};
|
||||
return result;
|
||||
} else {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
}
|
||||
|
||||
getBranchQualifiedName(branch) {
|
||||
return `refs/heads/${branch}`;
|
||||
}
|
||||
|
||||
getBranchQuery(branch) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
|
||||
return {
|
||||
query: queries.branch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getBranch(branch = this.branch) {
|
||||
// don't cache base branch to always get the latest data
|
||||
const fetchPolicy = branch === this.branch ? NO_CACHE : CACHE_FIRST;
|
||||
const { data } = await this.query({
|
||||
...this.getBranchQuery(branch),
|
||||
fetchPolicy,
|
||||
});
|
||||
return data.repository.branch;
|
||||
}
|
||||
|
||||
async patchRef(type, name, sha, opts = {}) {
|
||||
if (type !== 'heads') {
|
||||
return super.patchRef(type, name, sha, opts);
|
||||
}
|
||||
|
||||
const force = opts.force || false;
|
||||
|
||||
const branch = await this.getBranch(name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.updateBranch,
|
||||
variables: {
|
||||
input: { oid: sha, refId: branch.id, force },
|
||||
},
|
||||
});
|
||||
return data.updateRef.branch;
|
||||
}
|
||||
|
||||
async deleteBranch(branchName) {
|
||||
const branch = await this.getBranch(branchName);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.deleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
},
|
||||
update: store => store.data.delete(defaultDataIdFromObject(branch)),
|
||||
});
|
||||
|
||||
return data.deleteRef;
|
||||
}
|
||||
|
||||
getPullRequestQuery(number) {
|
||||
const { origin_repo_owner: owner, origin_repo_name: name } = this;
|
||||
|
||||
return {
|
||||
query: queries.pullRequest,
|
||||
variables: { owner, name, number },
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequest(number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestQuery(number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
// https://developer.github.com/v4/enum/pullrequeststate/
|
||||
// GraphQL state: [CLOSED, MERGED, OPEN]
|
||||
// REST API state: [closed, open]
|
||||
const state = data.repository.pullRequest.state === 'OPEN' ? 'open' : 'closed';
|
||||
return {
|
||||
...data.repository.pullRequest,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
getPullRequestAndBranchQuery(branch, number) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const { origin_repo_owner: origin_owner, origin_repo_name: origin_name } = this;
|
||||
|
||||
return {
|
||||
query: queries.pullRequestAndBranch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
origin_owner,
|
||||
origin_name,
|
||||
number,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequestAndBranch(branch, number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestAndBranchQuery(branch, number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
const { repository, origin } = data;
|
||||
return { branch: repository.branch, pullRequest: origin.pullRequest };
|
||||
}
|
||||
|
||||
async openPR({ number }) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.reopenPullRequest,
|
||||
variables: {
|
||||
reopenPullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult.reopenPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data.closePullRequest;
|
||||
}
|
||||
|
||||
async closePR({ number }) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequest,
|
||||
variables: {
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult.closePullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data.closePullRequest;
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName, slug) {
|
||||
try {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branchName = this.generateBranchName(contentKey);
|
||||
|
||||
const metadata = await this.retrieveMetadata(contentKey);
|
||||
if (metadata && metadata.pr) {
|
||||
const { branch, pullRequest } = await this.getPullRequestAndBranch(
|
||||
branchName,
|
||||
metadata.pr.number,
|
||||
);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequestAndDeleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: store => {
|
||||
store.data.delete(defaultDataIdFromObject(branch));
|
||||
store.data.delete(defaultDataIdFromObject(pullRequest));
|
||||
},
|
||||
});
|
||||
|
||||
return data.closePullRequest;
|
||||
} else {
|
||||
return await this.deleteBranch(branchName);
|
||||
}
|
||||
} catch (e) {
|
||||
const { graphQLErrors } = e;
|
||||
if (graphQLErrors && graphQLErrors.length > 0) {
|
||||
const branchNotFound = graphQLErrors.some(e => e.type === 'NOT_FOUND');
|
||||
if (branchNotFound) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async createPR(title, head) {
|
||||
const [repository, headReference] = await Promise.all([
|
||||
this.getRepository(this.origin_repo_owner, this.origin_repo_name),
|
||||
this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head,
|
||||
]);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createPullRequest,
|
||||
variables: {
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: API.DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: headReference,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult.createPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data.createPullRequest;
|
||||
return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
|
||||
}
|
||||
|
||||
async createBranch(branchName, sha) {
|
||||
const repository = await this.getRepository(this.repo_owner, this.repo_name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranch,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult.createRef;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName),
|
||||
data: branchData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { branch } = data.createRef;
|
||||
return branch;
|
||||
}
|
||||
|
||||
async createBranchAndPullRequest(branchName, sha, title) {
|
||||
const repository = await this.getRepository(this.origin_repo_owner, this.origin_repo_name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranchAndPullRequest,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: API.DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: branchName,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult.createRef;
|
||||
const { pullRequest } = mutationResult.createPullRequest;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
const pullRequestData = {
|
||||
repository: { ...pullRequest.repository, branch },
|
||||
origin: { ...pullRequest.repository, pullRequest },
|
||||
};
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName),
|
||||
data: branchData,
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestAndBranchQuery(branchName, pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data.createPullRequest;
|
||||
return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
|
||||
}
|
||||
|
||||
async getTree(sha) {
|
||||
if (!sha) {
|
||||
return Promise.resolve({ tree: [] });
|
||||
}
|
||||
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const variables = {
|
||||
owner,
|
||||
name,
|
||||
sha,
|
||||
};
|
||||
|
||||
// sha can be either for a commit or a tree
|
||||
const [commitTree, tree] = await Promise.all([
|
||||
this.client.query({
|
||||
query: queries.commitTree,
|
||||
variables,
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
}),
|
||||
this.client.query({
|
||||
query: queries.tree,
|
||||
variables,
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
}),
|
||||
]);
|
||||
|
||||
let entries = null;
|
||||
|
||||
if (commitTree.data.repository.commit.tree) {
|
||||
entries = commitTree.data.repository.commit.tree.entries;
|
||||
}
|
||||
|
||||
if (tree.data.repository.tree.entries) {
|
||||
entries = tree.data.repository.tree.entries;
|
||||
}
|
||||
|
||||
if (entries) {
|
||||
return { tree: entries.map(e => ({ ...e, mode: TREE_ENTRY_TYPE_TO_MODE[e.type] })) };
|
||||
}
|
||||
|
||||
return Promise.reject('Could not get tree');
|
||||
}
|
||||
|
||||
async getPullRequestCommits(number) {
|
||||
const { origin_repo_owner: owner, origin_repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.pullRequestCommits,
|
||||
variables: { owner, name, number },
|
||||
});
|
||||
const { nodes } = data.repository.pullRequest.commits;
|
||||
const commits = nodes.map(n => ({ ...n.commit, parents: n.commit.parents.nodes }));
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
async getFileSha(path, branch) {
|
||||
const { repo_owner: owner, repo_name: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.fileSha,
|
||||
variables: { owner, name, expression: `${branch}:${path}` },
|
||||
});
|
||||
|
||||
return data.repository.file.sha;
|
||||
}
|
||||
}
|
1
packages/netlify-cms-backend-github/src/fragmentTypes.js
Normal file
1
packages/netlify-cms-backend-github/src/fragmentTypes.js
Normal file
File diff suppressed because one or more lines are too long
76
packages/netlify-cms-backend-github/src/fragments.js
Normal file
76
packages/netlify-cms-backend-github/src/fragments.js
Normal file
@ -0,0 +1,76 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const repository = gql`
|
||||
fragment RepositoryParts on Repository {
|
||||
id
|
||||
}
|
||||
`;
|
||||
|
||||
export const blobWithText = gql`
|
||||
fragment BlobWithTextParts on Blob {
|
||||
id
|
||||
text
|
||||
is_binary: isBinary
|
||||
}
|
||||
`;
|
||||
|
||||
export const object = gql`
|
||||
fragment ObjectParts on GitObject {
|
||||
id
|
||||
sha: oid
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
fragment BranchParts on Ref {
|
||||
commit: target {
|
||||
...ObjectParts
|
||||
}
|
||||
id
|
||||
name
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${object}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
fragment PullRequestParts on PullRequest {
|
||||
id
|
||||
baseRefName
|
||||
body
|
||||
headRefName
|
||||
headRefOid
|
||||
number
|
||||
state
|
||||
title
|
||||
merged_at: mergedAt
|
||||
repository {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${repository}
|
||||
`;
|
||||
|
||||
export const treeEntry = gql`
|
||||
fragment TreeEntryParts on TreeEntry {
|
||||
path: name
|
||||
sha: oid
|
||||
type
|
||||
mode
|
||||
}
|
||||
`;
|
||||
|
||||
export const fileEntry = gql`
|
||||
fragment FileEntryParts on TreeEntry {
|
||||
name
|
||||
sha: oid
|
||||
blob: object {
|
||||
... on Blob {
|
||||
size: byteSize
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -4,6 +4,7 @@ import semaphore from 'semaphore';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API from './API';
|
||||
import GraphQLAPI from './GraphQLAPI';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
@ -59,12 +60,13 @@ export default class GitHub {
|
||||
}
|
||||
this.originRepo = config.getIn(['backend', 'repo'], '');
|
||||
} else {
|
||||
this.repo = config.getIn(['backend', 'repo'], '');
|
||||
this.repo = this.originRepo = config.getIn(['backend', 'repo'], '');
|
||||
}
|
||||
this.branch = config.getIn(['backend', 'branch'], 'master').trim();
|
||||
this.api_root = config.getIn(['backend', 'api_root'], 'https://api.github.com');
|
||||
this.token = '';
|
||||
this.squash_merges = config.getIn(['backend', 'squash_merges']);
|
||||
this.use_graphql = config.getIn(['backend', 'use_graphql']);
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
@ -155,11 +157,12 @@ export default class GitHub {
|
||||
|
||||
async authenticate(state) {
|
||||
this.token = state.token;
|
||||
this.api = new API({
|
||||
const apiCtor = this.use_graphql ? GraphQLAPI : API;
|
||||
this.api = new apiCtor({
|
||||
token: this.token,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
originRepo: this.useOpenAuthoring ? this.originRepo : undefined,
|
||||
originRepo: this.originRepo,
|
||||
api_root: this.api_root,
|
||||
squash_merges: this.squash_merges,
|
||||
useOpenAuthoring: this.useOpenAuthoring,
|
||||
@ -191,6 +194,9 @@ export default class GitHub {
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
if (typeof this.api.reset === 'function') {
|
||||
return this.api.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -243,7 +249,7 @@ export default class GitHub {
|
||||
|
||||
// Fetches a single entry.
|
||||
getEntry(collection, slug, path) {
|
||||
const repoURL = `/repos/${this.useOpenAuthoring ? this.originRepo : this.repo}`;
|
||||
const repoURL = `/repos/${this.originRepo}`;
|
||||
return this.api.readFile(path, null, { repoURL }).then(data => ({
|
||||
file: { path },
|
||||
data,
|
||||
|
109
packages/netlify-cms-backend-github/src/mutations.js
Normal file
109
packages/netlify-cms-backend-github/src/mutations.js
Normal file
@ -0,0 +1,109 @@
|
||||
import gql from 'graphql-tag';
|
||||
import * as fragments from './fragments';
|
||||
|
||||
// updateRef only works for branches at the moment
|
||||
export const updateBranch = gql`
|
||||
mutation updateRef($input: UpdateRefInput!) {
|
||||
updateRef(input: $input) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// deleteRef only works for branches at the moment
|
||||
const deleteRefMutationPart = `
|
||||
deleteRef(input: $deleteRefInput) {
|
||||
clientMutationId
|
||||
}
|
||||
`;
|
||||
export const deleteBranch = gql`
|
||||
mutation deleteRef($deleteRefInput: DeleteRefInput!) {
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
`;
|
||||
|
||||
const closePullRequestMutationPart = `
|
||||
closePullRequest(input: $closePullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const closePullRequest = gql`
|
||||
mutation closePullRequestAndDeleteBranch($closePullRequestInput: ClosePullRequestInput!) {
|
||||
${closePullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const closePullRequestAndDeleteBranch = gql`
|
||||
mutation closePullRequestAndDeleteBranch(
|
||||
$closePullRequestInput: ClosePullRequestInput!
|
||||
$deleteRefInput: DeleteRefInput!
|
||||
) {
|
||||
${closePullRequestMutationPart}
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
const createPullRequestMutationPart = `
|
||||
createPullRequest(input: $createPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const createPullRequest = gql`
|
||||
mutation createPullRequest($createPullRequestInput: CreatePullRequestInput!) {
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const createBranch = gql`
|
||||
mutation createBranch($createRefInput: CreateRefInput!) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// createRef only works for branches at the moment
|
||||
export const createBranchAndPullRequest = gql`
|
||||
mutation createBranchAndPullRequest(
|
||||
$createRefInput: CreateRefInput!
|
||||
$createPullRequestInput: CreatePullRequestInput!
|
||||
) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const reopenPullRequest = gql`
|
||||
mutation reopenPullRequest($reopenPullRequestInput: ReopenPullRequestInput!) {
|
||||
reopenPullRequest(input: $reopenPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
274
packages/netlify-cms-backend-github/src/queries.js
Normal file
274
packages/netlify-cms-backend-github/src/queries.js
Normal file
@ -0,0 +1,274 @@
|
||||
import gql from 'graphql-tag';
|
||||
import * as fragments from './fragments';
|
||||
|
||||
export const repoPermission = gql`
|
||||
query repoPermission($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
viewerPermission
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
export const user = gql`
|
||||
query {
|
||||
viewer {
|
||||
id
|
||||
avatar_url: avatarUrl
|
||||
name
|
||||
login
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const blob = gql`
|
||||
query blob($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
... on Blob {
|
||||
...BlobWithTextParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.blobWithText}
|
||||
`;
|
||||
|
||||
export const unpublishedBranchFile = gql`
|
||||
query unpublishedBranchFile(
|
||||
$headOwner: String!
|
||||
$headRepoName: String!
|
||||
$headExpression: String!
|
||||
$baseOwner: String!
|
||||
$baseRepoName: String!
|
||||
$baseExpression: String!
|
||||
) {
|
||||
head: repository(owner: $headOwner, name: $headRepoName) {
|
||||
...RepositoryParts
|
||||
object(expression: $headExpression) {
|
||||
... on Blob {
|
||||
...BlobWithTextParts
|
||||
}
|
||||
}
|
||||
}
|
||||
base: repository(owner: $baseOwner, name: $baseRepoName) {
|
||||
...RepositoryParts
|
||||
object(expression: $baseExpression) {
|
||||
... on Blob {
|
||||
id
|
||||
oid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.blobWithText}
|
||||
`;
|
||||
|
||||
export const statues = gql`
|
||||
query statues($owner: String!, $name: String!, $sha: GitObjectID!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(oid: $sha) {
|
||||
...ObjectParts
|
||||
... on Commit {
|
||||
status {
|
||||
id
|
||||
contexts {
|
||||
id
|
||||
context
|
||||
state
|
||||
target_url: targetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
|
||||
export const files = gql`
|
||||
query files($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
object(expression: $expression) {
|
||||
...ObjectParts
|
||||
... on Tree {
|
||||
entries {
|
||||
...FileEntryParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
${fragments.fileEntry}
|
||||
`;
|
||||
|
||||
export const unpublishedPrBranches = gql`
|
||||
query unpublishedPrBranches($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
refs(refPrefix: "refs/heads/cms/", last: 50) {
|
||||
nodes {
|
||||
id
|
||||
associatedPullRequests(last: 50, states: OPEN) {
|
||||
nodes {
|
||||
id
|
||||
headRef {
|
||||
id
|
||||
name
|
||||
prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
const branchQueryPart = `
|
||||
branch: ref(qualifiedName: $qualifiedName) {
|
||||
...BranchParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const branch = gql`
|
||||
query branch($owner: String!, $name: String!, $qualifiedName: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const repository = gql`
|
||||
query repository($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
const pullRequestQueryPart = `
|
||||
pullRequest(number: $number) {
|
||||
...PullRequestParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
query pullRequest($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequestAndBranch = gql`
|
||||
query pullRequestAndBranch($owner: String!, $name: String!, $origin_owner: String!, $origin_name: String!, $qualifiedName: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
origin: repository(owner: $origin_owner, name: $origin_name) {
|
||||
...RepositoryParts
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const commitTree = gql`
|
||||
query commitTree($owner: String!, $name: String!, $sha: GitObjectID!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
commit: object(oid: $sha) {
|
||||
...ObjectParts
|
||||
... on Commit {
|
||||
tree {
|
||||
...ObjectParts
|
||||
entries {
|
||||
...TreeEntryParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
${fragments.treeEntry}
|
||||
`;
|
||||
|
||||
export const tree = gql`
|
||||
query tree($owner: String!, $name: String!, $sha: GitObjectID!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
tree: object(oid: $sha) {
|
||||
...ObjectParts
|
||||
... on Tree {
|
||||
entries {
|
||||
...TreeEntryParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
${fragments.treeEntry}
|
||||
`;
|
||||
|
||||
export const pullRequestCommits = gql`
|
||||
query pullRequestCommits($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
pullRequest(number: $number) {
|
||||
id
|
||||
commits(last: 100) {
|
||||
nodes {
|
||||
id
|
||||
commit {
|
||||
...ObjectParts
|
||||
parents(last: 100) {
|
||||
nodes {
|
||||
...ObjectParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
||||
|
||||
export const fileSha = gql`
|
||||
query fileSha($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
file: object(expression: $expression) {
|
||||
...ObjectParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.object}
|
||||
`;
|
@ -56,22 +56,24 @@ export const parseLinkHeader = flow([
|
||||
fromPairs,
|
||||
]);
|
||||
|
||||
export const getPaginatedRequestIterator = (url, options = {}, linkHeaderRelName = 'next') => {
|
||||
let req = unsentRequest.fromFetchArguments(url, options);
|
||||
const next = async () => {
|
||||
if (!req) {
|
||||
return { done: true };
|
||||
}
|
||||
export const getAllResponses = async (url, options = {}, linkHeaderRelName = 'next') => {
|
||||
const maxResponses = 30;
|
||||
let responseCount = 1;
|
||||
|
||||
let req = unsentRequest.fromFetchArguments(url, options);
|
||||
|
||||
const pageResponses = [];
|
||||
|
||||
while (req && responseCount < maxResponses) {
|
||||
const pageResponse = await unsentRequest.performRequest(req);
|
||||
const linkHeader = pageResponse.headers.get('Link');
|
||||
const nextURL = linkHeader && parseLinkHeader(linkHeader)[linkHeaderRelName];
|
||||
req = nextURL && unsentRequest.fromURL(nextURL);
|
||||
return { value: pageResponse };
|
||||
};
|
||||
return {
|
||||
[Symbol.asyncIterator]: () => ({
|
||||
next,
|
||||
}),
|
||||
};
|
||||
|
||||
const { headers = {} } = options;
|
||||
req = nextURL && unsentRequest.fromFetchArguments(nextURL, { headers });
|
||||
pageResponses.push(pageResponse);
|
||||
responseCount++;
|
||||
}
|
||||
|
||||
return pageResponses;
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
import unsentRequest from './unsentRequest';
|
||||
import {
|
||||
filterByPropExtension,
|
||||
getPaginatedRequestIterator,
|
||||
getAllResponses,
|
||||
parseLinkHeader,
|
||||
parseResponse,
|
||||
responseParser,
|
||||
@ -72,7 +72,7 @@ export {
|
||||
unsentRequest,
|
||||
filterByPropExtension,
|
||||
parseLinkHeader,
|
||||
getPaginatedRequestIterator,
|
||||
getAllResponses,
|
||||
parseResponse,
|
||||
responseParser,
|
||||
loadScript,
|
||||
|
306
yarn.lock
306
yarn.lock
@ -1857,6 +1857,19 @@
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^2.1.0"
|
||||
|
||||
"@octokit/request@^5.0.0":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.0.2.tgz#59a920451f24811c016ddc507adcc41aafb2dca5"
|
||||
integrity sha512-z1BQr43g4kOL4ZrIVBMHwi68Yg9VbkRUyuAgqCp1rU3vbYa69+2gIld/+gHclw15bJWQnhqqyEb7h5a5EqgZ0A==
|
||||
dependencies:
|
||||
"@octokit/endpoint" "^5.1.0"
|
||||
"@octokit/request-error" "^1.0.1"
|
||||
deprecation "^2.0.0"
|
||||
is-plain-object "^3.0.0"
|
||||
node-fetch "^2.3.0"
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^3.0.0"
|
||||
|
||||
"@octokit/rest@^16.16.0":
|
||||
version "16.28.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.28.2.tgz#3fc3b8700046ab29ab1e2a4bdf49f89e94f7ba27"
|
||||
@ -1876,6 +1889,25 @@
|
||||
universal-user-agent "^2.0.0"
|
||||
url-template "^2.0.8"
|
||||
|
||||
"@octokit/rest@^16.28.7":
|
||||
version "16.28.7"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.28.7.tgz#a2c2db5b318da84144beba82d19c1a9dbdb1a1fa"
|
||||
integrity sha512-cznFSLEhh22XD3XeqJw51OLSfyL2fcFKUO+v2Ep9MTAFfFLS1cK1Zwd1yEgQJmJoDnj4/vv3+fGGZweG+xsbIA==
|
||||
dependencies:
|
||||
"@octokit/request" "^5.0.0"
|
||||
"@octokit/request-error" "^1.0.2"
|
||||
atob-lite "^2.0.0"
|
||||
before-after-hook "^2.0.0"
|
||||
btoa-lite "^1.0.0"
|
||||
deprecation "^2.0.0"
|
||||
lodash.get "^4.4.2"
|
||||
lodash.set "^4.3.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
octokit-pagination-methods "^1.1.0"
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^3.0.0"
|
||||
url-template "^2.0.8"
|
||||
|
||||
"@sheerun/mutationobserver-shim@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
|
||||
@ -1971,6 +2003,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.10.tgz#51babf9c7deadd5343620055fc8aff7995c8b031"
|
||||
integrity sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==
|
||||
|
||||
"@types/node@>=6":
|
||||
version "12.6.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c"
|
||||
integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||
@ -2026,6 +2063,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
|
||||
integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==
|
||||
|
||||
"@types/zen-observable@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
|
||||
integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
|
||||
|
||||
"@webassemblyjs/ast@1.8.5":
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
|
||||
@ -2172,6 +2214,21 @@
|
||||
"@webassemblyjs/wast-parser" "1.8.5"
|
||||
"@xtuc/long" "4.2.2"
|
||||
|
||||
"@wry/context@^0.4.0":
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8"
|
||||
integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==
|
||||
dependencies:
|
||||
"@types/node" ">=6"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@wry/equality@^0.1.2":
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909"
|
||||
integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@xtuc/ieee754@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||
@ -2353,6 +2410,85 @@ anymatch@^2.0.0:
|
||||
micromatch "^3.1.4"
|
||||
normalize-path "^2.1.1"
|
||||
|
||||
apollo-cache-inmemory@^1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.2.tgz#bbf2e4e1eacdf82b2d526f5c2f3b37e5acee3c5e"
|
||||
integrity sha512-AyCl3PGFv5Qv1w4N9vlg63GBPHXgMCekZy5mhlS042ji0GW84uTySX+r3F61ZX3+KM1vA4m9hQyctrEGiv5XjQ==
|
||||
dependencies:
|
||||
apollo-cache "^1.3.2"
|
||||
apollo-utilities "^1.3.2"
|
||||
optimism "^0.9.0"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-cache@1.3.2, apollo-cache@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a"
|
||||
integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg==
|
||||
dependencies:
|
||||
apollo-utilities "^1.3.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-client@^2.6.3:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
|
||||
integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
|
||||
dependencies:
|
||||
"@types/zen-observable" "^0.8.0"
|
||||
apollo-cache "1.3.2"
|
||||
apollo-link "^1.0.0"
|
||||
apollo-utilities "1.3.2"
|
||||
symbol-observable "^1.0.2"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
zen-observable "^0.8.0"
|
||||
|
||||
apollo-link-context@^1.0.18:
|
||||
version "1.0.18"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.18.tgz#9e700e3314da8ded50057fee0a18af2bfcedbfc3"
|
||||
integrity sha512-aG5cbUp1zqOHHQjAJXG7n/izeMQ6LApd/whEF5z6qZp5ATvcyfSNkCfy3KRJMMZZ3iNfVTs6jF+IUA8Zvf+zeg==
|
||||
dependencies:
|
||||
apollo-link "^1.2.12"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-link-http-common@^0.2.14:
|
||||
version "0.2.14"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
|
||||
integrity sha512-v6mRU1oN6XuX8beVIRB6OpF4q1ULhSnmy7ScnHnuo1qV6GaFmDcbdvXqxIkAV1Q8SQCo2lsv4HeqJOWhFfApOg==
|
||||
dependencies:
|
||||
apollo-link "^1.2.12"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-link-http@^1.5.15:
|
||||
version "1.5.15"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.15.tgz#106ab23bb8997bd55965d05855736d33119652cf"
|
||||
integrity sha512-epZFhCKDjD7+oNTVK3P39pqWGn4LEhShAoA1Q9e2tDrBjItNfviiE33RmcLcCURDYyW5JA6SMgdODNI4Is8tvQ==
|
||||
dependencies:
|
||||
apollo-link "^1.2.12"
|
||||
apollo-link-http-common "^0.2.14"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-link@^1.0.0, apollo-link@^1.2.12:
|
||||
version "1.2.12"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
|
||||
integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==
|
||||
dependencies:
|
||||
apollo-utilities "^1.3.0"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
zen-observable-ts "^0.8.19"
|
||||
|
||||
apollo-utilities@1.3.2, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
|
||||
integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
|
||||
dependencies:
|
||||
"@wry/equality" "^0.1.2"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
aproba@^1.0.3, aproba@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||
@ -2905,6 +3041,11 @@ before-after-hook@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d"
|
||||
integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg==
|
||||
|
||||
before-after-hook@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635"
|
||||
integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==
|
||||
|
||||
big.js@^3.1.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
|
||||
@ -4102,10 +4243,10 @@ cyclist@~0.2.2:
|
||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
|
||||
|
||||
cypress@^3.1.5:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.3.1.tgz#8a127b1d9fa74bff21f111705abfef58d595fdef"
|
||||
integrity sha512-JIo47ZD9P3jAw7oaK7YKUoODzszJbNw41JmBrlMMiupHOlhmXvZz75htuo7mfRFPC9/1MDQktO4lX/V2+a6lGQ==
|
||||
cypress@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.4.1.tgz#ca2e4e9864679da686c6a6189603efd409664c30"
|
||||
integrity sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg==
|
||||
dependencies:
|
||||
"@cypress/listr-verbose-renderer" "0.4.1"
|
||||
"@cypress/xvfb" "1.2.4"
|
||||
@ -4120,20 +4261,19 @@ cypress@^3.1.5:
|
||||
execa "0.10.0"
|
||||
executable "4.1.1"
|
||||
extract-zip "1.6.7"
|
||||
fs-extra "4.0.1"
|
||||
fs-extra "5.0.0"
|
||||
getos "3.1.1"
|
||||
glob "7.1.3"
|
||||
is-ci "1.2.1"
|
||||
is-installed-globally "0.1.0"
|
||||
lazy-ass "1.6.0"
|
||||
listr "0.12.0"
|
||||
lodash "4.17.11"
|
||||
lodash "4.17.15"
|
||||
log-symbols "2.2.0"
|
||||
minimist "1.2.0"
|
||||
moment "2.24.0"
|
||||
ramda "0.24.1"
|
||||
request "2.88.0"
|
||||
request-progress "0.4.0"
|
||||
request-progress "3.0.0"
|
||||
supports-color "5.5.0"
|
||||
tmp "0.1.0"
|
||||
url "0.11.0"
|
||||
@ -4574,6 +4714,11 @@ dot-prop@^4.1.1, dot-prop@^4.2.0:
|
||||
dependencies:
|
||||
is-obj "^1.0.0"
|
||||
|
||||
dotenv@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440"
|
||||
integrity sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==
|
||||
|
||||
duplexer@^0.1.1, duplexer@~0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
|
||||
@ -4848,6 +4993,13 @@ eslint-plugin-babel@^5.3.0:
|
||||
dependencies:
|
||||
eslint-rule-composer "^0.3.0"
|
||||
|
||||
eslint-plugin-cypress@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.6.0.tgz#c726dd1a312cd5234de94765ca79718a14edf0ef"
|
||||
integrity sha512-/YmvhYFqjFLYw7llDl56U9KW+Z25TZJjDofT47aUnAFRRvOhj1y2nxJzZ1ZTnqBO1ja8gwRhtdT7LfTLg9ANJw==
|
||||
dependencies:
|
||||
globals "^11.12.0"
|
||||
|
||||
eslint-plugin-emotion@^10.0.7:
|
||||
version "10.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-emotion/-/eslint-plugin-emotion-10.0.7.tgz#e14d54a2f23c9701d66851a87a5eb35adb10c2a7"
|
||||
@ -5577,13 +5729,13 @@ from@~0:
|
||||
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
|
||||
integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
|
||||
|
||||
fs-extra@4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880"
|
||||
integrity sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=
|
||||
fs-extra@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
|
||||
integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
jsonfile "^3.0.0"
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^7.0.0:
|
||||
@ -5595,6 +5747,15 @@ fs-extra@^7.0.0:
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
|
||||
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-minipass@^1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
|
||||
@ -5815,18 +5976,6 @@ glob-to-regexp@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
|
||||
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
|
||||
|
||||
glob@7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
|
||||
version "7.1.4"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
|
||||
@ -5890,7 +6039,7 @@ global@^4.3.0:
|
||||
min-document "^2.19.0"
|
||||
process "^0.11.10"
|
||||
|
||||
globals@^11.1.0, globals@^11.7.0:
|
||||
globals@^11.1.0, globals@^11.12.0, globals@^11.7.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
@ -5974,6 +6123,23 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
||||
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
||||
|
||||
graceful-fs@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
|
||||
integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==
|
||||
|
||||
graphql-tag@^2.10.1:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02"
|
||||
integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==
|
||||
|
||||
graphql@^14.4.2:
|
||||
version "14.4.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3"
|
||||
integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==
|
||||
dependencies:
|
||||
iterall "^1.2.2"
|
||||
|
||||
gray-matter@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454"
|
||||
@ -7072,6 +7238,11 @@ istanbul-reports@^2.1.1:
|
||||
dependencies:
|
||||
handlebars "^4.1.2"
|
||||
|
||||
iterall@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
|
||||
integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==
|
||||
|
||||
jest-changed-files@^24.8.0:
|
||||
version "24.8.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
|
||||
@ -7601,13 +7772,6 @@ json5@^2.1.0:
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
jsonfile@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
|
||||
integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonfile@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
|
||||
@ -7952,7 +8116,12 @@ lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash@4.17.11, lodash@^4.1.1, lodash@^4.11.2, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1:
|
||||
lodash@4.17.15:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@^4.1.1, lodash@^4.11.2, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
@ -8593,11 +8762,6 @@ node-addon-api@^1.6.0:
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.6.3.tgz#3998d4593e2dca2ea82114670a4eb003386a9fe1"
|
||||
integrity sha512-FXWH6mqjWgU8ewuahp4spec8LkroFZK2NicOv6bNwZC3kcwZUI8LeZdG80UzTSLLhK4T7MsgNwlYDVRlDdfTDg==
|
||||
|
||||
node-eta@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/node-eta/-/node-eta-0.1.1.tgz#4066109b39371c761c72b7ebda9a9ea0a5de121f"
|
||||
integrity sha1-QGYQmzk3HHYccrfr2pqeoKXeEh8=
|
||||
|
||||
node-fetch-npm@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7"
|
||||
@ -9027,6 +9191,13 @@ opn@^5.5.0:
|
||||
dependencies:
|
||||
is-wsl "^1.1.0"
|
||||
|
||||
optimism@^0.9.0:
|
||||
version "0.9.6"
|
||||
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.9.6.tgz#5621195486b294c3bfc518d17ac47767234b029f"
|
||||
integrity sha512-bWr/ZP32UgFCQAoSkz33XctHwpq2via2sBvGvO5JIlrU8gaiM0LvoKj3QMle9LWdSKlzKik8XGSerzsdfYLNxA==
|
||||
dependencies:
|
||||
"@wry/context" "^0.4.0"
|
||||
|
||||
optimist@0.6.x, optimist@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||
@ -10754,13 +10925,12 @@ replace-ext@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
|
||||
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
|
||||
|
||||
request-progress@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.4.0.tgz#c1954e39086aa85269c5660bcee0142a6a70d7e7"
|
||||
integrity sha1-wZVOOQhqqFJpxWYLzuAUKmpw1+c=
|
||||
request-progress@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
|
||||
integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=
|
||||
dependencies:
|
||||
node-eta "^0.1.1"
|
||||
throttleit "^0.0.2"
|
||||
throttleit "^1.0.0"
|
||||
|
||||
request-promise-core@1.1.2:
|
||||
version "1.1.2"
|
||||
@ -11200,6 +11370,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
|
||||
|
||||
simple-git@^1.124.0:
|
||||
version "1.124.0"
|
||||
resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.124.0.tgz#10a73cc1af303832b5c11720d4256e134fba35ca"
|
||||
integrity sha512-ks9mBoO4ODQy/xGLC8Cc+YDvj/hho/IKgPhi6h5LI/sA+YUdHc3v0DEoHzM29VmulubpGCxMJUSFmyXNsjNMEA==
|
||||
dependencies:
|
||||
debug "^4.0.1"
|
||||
|
||||
simple-html-tokenizer@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz#05c2eec579ffffe145a030ac26cfea61b980fabe"
|
||||
@ -11963,7 +12140,7 @@ symbol-observable@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
|
||||
integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=
|
||||
|
||||
symbol-observable@^1.2.0:
|
||||
symbol-observable@^1.0.2, symbol-observable@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||
@ -12085,10 +12262,10 @@ throat@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
|
||||
integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
|
||||
|
||||
throttleit@^0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
|
||||
integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8=
|
||||
throttleit@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
|
||||
|
||||
through2@^2.0.0, through2@^2.0.2:
|
||||
version "2.0.5"
|
||||
@ -12309,7 +12486,14 @@ truncate-utf8-bytes@^1.0.0:
|
||||
dependencies:
|
||||
utf8-byte-length "^1.0.1"
|
||||
|
||||
tslib@^1.9.0:
|
||||
ts-invariant@^0.4.0:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
|
||||
integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
|
||||
tslib@^1.9.0, tslib@^1.9.3:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
@ -12566,6 +12750,13 @@ universal-user-agent@^2.0.0, universal-user-agent@^2.1.0:
|
||||
dependencies:
|
||||
os-name "^3.0.0"
|
||||
|
||||
universal-user-agent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-3.0.0.tgz#4cc88d68097bffd7ac42e3b7c903e7481424b4b9"
|
||||
integrity sha512-T3siHThqoj5X0benA5H0qcDnrKGXzU8TKoX15x/tQHw1hQBvIEBHjxQ2klizYsqBOO/Q+WuxoQUihadeeqDnoA==
|
||||
dependencies:
|
||||
os-name "^3.0.0"
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
@ -13222,3 +13413,16 @@ yauzl@2.4.1:
|
||||
integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=
|
||||
dependencies:
|
||||
fd-slicer "~1.0.1"
|
||||
|
||||
zen-observable-ts@^0.8.19:
|
||||
version "0.8.19"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694"
|
||||
integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
zen-observable "^0.8.0"
|
||||
|
||||
zen-observable@^0.8.0:
|
||||
version "0.8.14"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7"
|
||||
integrity sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g==
|
||||
|
Loading…
x
Reference in New Issue
Block a user