Exemplo n.º 1
0
export default Service.extend(Evented, {

    ghostPaths: injectService(),
    session: injectService(),

    // this service is responsible for managing tour item visibility and syncing
    // the viewed state with the server
    //
    // tour items need to be centrally defined here so that we have a single
    // source of truth for marking all tour items as viewed
    //
    // a {{gh-tour-item "unique-id"}} component can be inserted in any template,
    // this will use the tour service to grab content and determine visibility
    // with the component in control of rendering the throbber/controlling the
    // modal - this allows the component lifecycle hooks to perform automatic
    // display/cleanup when the relevant UI is visible.

    viewed: [],

    // IDs should **NOT** be changed if they have been part of a release unless
    // the re-display of the throbber should be forced. In that case it may be
    // useful to add a version number eg. `my-feature` -> `my-feature-v2`.
    // Format is as follows:
    //
    // {
    //     id: 'test',
    //     title: 'This is a test',
    //     message: 'This is a test of our <strong>feature tour</strong> feature'
    // }
    //
    // TODO: it may be better to keep this configuration elsewhere to keep the
    // service clean. Eventually we'll want apps to be able to register their
    // own throbbers and tour content
    throbbers: [],

    init() {
        let adminUrl = `${window.location.origin}${this.get('ghostPaths.url').admin()}`;
        let adminDisplayUrl = adminUrl.replace(`${window.location.protocol}//`, '');

        this.throbbers = [{
            id: 'getting-started',
            title: 'Getting started with Ghost',
            message: `This is your admin area! You'll find all of your content, users and settings right here. You can come back any time by visiting <a href="${adminUrl}" target="_blank">${adminDisplayUrl}</a>`
        }, {
            id: 'using-the-editor',
            title: 'Using the Ghost editor',
            message: 'Ghost uses Markdown to allow you to write and format content quickly and easily. This toolbar also helps! Hit the <strong>?</strong> icon for more editor shortcuts.'
        }, {
            id: 'static-post',
            title: 'Turning posts into pages',
            message: 'Static pages are permanent pieces of content which live outside of your usual stream of posts, for example an \'about\' or \'contact\' page.'
        }, {
            id: 'featured-post',
            title: 'Setting a featured post',
            message: 'Depending on your theme, featured posts receive special styling to make them stand out as a particularly important or emphasised story.'
        }, {
            id: 'upload-a-theme',
            title: 'Customising your publication',
            message: 'Using custom themes, you can completely control the look and feel of your site to suit your branch. Here\'s a full guide to help: <strong><a href="https://themes.ghost.org" target="_blank">https://themes.ghost.org</a></strong>'
        }];
    },

    _activeThrobbers: computed('viewed.[]', 'throbbers.[]', function () {
        // return throbbers that haven't been viewed
        let viewed = this.get('viewed');
        let throbbers = this.get('throbbers');

        return throbbers.reject((throbber) => {
            return viewed.includes(throbber.id);
        });
    }),

    // retrieve the IDs of the viewed throbbers from the server, always returns
    // a promise
    fetchViewed() {
        return this.get('session.user').then((user) => {
            let viewed = user.get('tour') || [];

            this.set('viewed', viewed);

            return viewed;
        });
    },

    // save the list of viewed throbbers to the server overwriting the
    // entire list
    syncViewed() {
        let viewed = this.get('viewed');

        return this.get('session.user').then((user) => {
            user.set('tour', viewed);

            return user.save();
        });
    },

    // returns throbber content for a given ID only if that throbber hasn't been
    // viewed. Used by the {{gh-tour-item}} component to determine visibility
    activeThrobber(id) {
        let activeThrobbers = this.get('_activeThrobbers');
        return activeThrobbers.findBy('id', id);
    },

    // when a throbber is opened the component will call this method to mark
    // it as viewed and sync with the server. Always returns a promise
    markThrobberAsViewed(id) {
        let viewed = this.get('viewed');

        if (!viewed.includes(id)) {
            viewed.pushObject(id);
            this.trigger('viewed', id);
            return this.syncViewed();
        } else {
            return RSVP.resolve(true);
        }
    },

    // opting-out will use the list of IDs defined in this file making it the
    // single-source-of-truth and allowing future client updates to control when
    // new UI should be surfaced through tour items
    optOut() {
        let allThrobberIds = this.get('throbbers').mapBy('id');

        this.set('viewed', allThrobberIds);
        this.trigger('optOut');

        return this.syncViewed();
    },

    // this is not used anywhere at the moment but it's useful to use via ember
    // inspector as a reset mechanism
    reEnable() {
        this.set('viewed', []);
        return this.syncViewed();
    }

});
Exemplo n.º 2
0
export default Service.extend({
  auth: inject(),

  init() {
    this._super();
    this.set('fields', []);
    this.set('hasNotBeenActive', true);
    this._fieldSources = Object.create(null);
    this._highlightedFieldId = null;
    this._primaryPageSources = Object.create(null);
  },

  registerField(sourceId, field) {
    this._fieldSources[sourceId] = field;
    scheduleOnce('afterRender', this, this._handleFieldUpdates);
  },

  unregisterField(sourceId) {
    this._fieldSources[sourceId] = null;
    scheduleOnce('afterRender', this, this._handleFieldUpdates);
  },

  _handleFieldUpdates() {
    let sources = this._fieldSources;
    let fields = Object.create(null);
    for (let fieldId in sources) {
      let field = sources[fieldId];
      if (field) {
        fields[field.id] = field;
        field.highlight = this._highlightedFieldId === field.id;
      }
    }
    this.set('fields', Object.keys(fields).map(id => fields[id]));
  },

  requested: readOnly('topController.toolsRequested'),
  _editing: readOnly('topController.editing'),
  editing: computed('_editing', 'active', function() {
    return this.get('_editing') && this.get('active');
  }),
  previewMode: readOnly('topController.previewMode'),
  tab: readOnly('topController.toolsTab'),

  // This is to deal with the flickr that appears when working fastboot is enabled
  // the activation animation triggers when browser javascript is processed
  // and I apologize for the observer
  afterActivated: Ember.observer('topController.toolsRequested', 'available', function() {
    if (this.get('topController.toolsRequested') && this.get('available')) {
      this.set('hasNotBeenActive', false);
    }
  }),

  available: computed('auth.user.isEditor', function() {
    if (!window.janrain) {
      return !!this.get('auth.user');
    }
    return !!this.get('auth.user.isEditor');
  }),

  active: computed('requested', 'available', function() {
    return this.get('requested') && this.get('available');
  }),

  // The following actions are computed properties that return
  // closures so they can be passed around directly in templates.

  activate: computed(function() {
    return (state) => this.get('topController').set('toolsRequested', state);
  }),

  toggle: computed(function() {
    return () => this.get('topController').set('toolsRequested', !this.get('requested'));
  }),

  edit: computed(function() {
    return state => this.get('topController').set('editing', state);
  }),

  preview: computed(function() {
    return mode => {
      if (mode !== this.get('previewMode')) {
        this.set('openedField', null);
        this.get('topController').set('previewMode', mode);
      }
    };
  }),

  highlightField: computed(function() {
    return field => {
      this._highlightedFieldId = field ? field.id : null;
      scheduleOnce('afterRender', this, this._handleFieldUpdates);
    };
  }),

  openField: computed(function() {
    return field => {
      this.set('openedField', field);
      this.get('switchToTab')('compose');
    };
  }),

  switchToTab: computed(function() {
    return tab => {
      this.get('topController').set('toolsTab', tab);
    };
  }),

  setPrimaryPage(page) {
    this._primaryPageSources[guidFor(page)] = page;
    scheduleOnce('afterRender', this, this._updatePrimaryPage);
  },

  clearPrimaryPage(page) {
    delete this._primaryPageSources[guidFor(page)];
    scheduleOnce('afterRender', this, this._updatePrimaryPage);
  },

  _updatePrimaryPage() {
    let key = Object.keys(this._primaryPageSources)[0];
    if (key) {
      this.set('primaryPage', this._primaryPageSources[key]);
    } else {
      this.set('primaryPage', null);
    }
  }

});
Exemplo n.º 3
0
/* jshint expr:true */
import { expect } from 'chai';
import {
  describeComponent,
  it
} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import run from 'ember-runloop';
import Service from 'ember-service';
import {A as emberA} from 'ember-array/utils';

let notificationsStub = Service.extend({
    alerts: emberA()
});

describeComponent(
    'gh-alerts',
    'Integration: Component: gh-alerts',
    {
        integration: true
    },
    function () {
        beforeEach(function () {
            this.register('service:notifications', notificationsStub);
            this.inject.service('notifications', {as: 'notifications'});

            this.set('notifications.alerts', [
                {message: 'First', type: 'error'},
                {message: 'Second', type: 'warn'}
            ]);
        });
Exemplo n.º 4
0
export default Service.extend({
  userActivity: injectService('ember-user-activity@user-activity'),

  _debouncedTimeout: null,

  activeEvents: ['userActive'],
  IDLE_TIMEOUT: 600000, // 10 minutes
  isIdle: false,

  _setupListeners(method) {
    let userActivity = this.get('userActivity');
    this.get('activeEvents').forEach((event) => {
      userActivity[method](event, this, this.resetTimeout);
    });
  },

  init() {
    if (testing) { // Shorter debounce in testing mode
      this.set('IDLE_TIMEOUT', 10);
    }
    this._setupListeners('on');
    this.resetTimeout();
  },

  willDestroy() {
    this._setupListeners('off');
    if (this._debouncedTimeout) {
      cancel(this._debouncedTimeout);
    }
  },

  resetTimeout() {
    this.set('isIdle', false);
    this._debouncedTimeout = debounce(this, this.setIdle, this.get('IDLE_TIMEOUT'));
  },

  setIdle() {
    this.set('isIdle', true);
  }
});
Exemplo n.º 5
0
import RSVP from 'rsvp';
import Service from 'ember-service';
import injectService from 'ember-service/inject';

const {resolve} = RSVP;

export default Service.extend({
    ghostPaths: injectService(),
    ajax: injectService(),

    generateSlug(slugType, textToSlugify) {
        let url;

        if (!textToSlugify) {
            return resolve('');
        }

        url = this.get('ghostPaths.url').api('slugs', slugType, encodeURIComponent(textToSlugify));

        return this.get('ajax').request(url).then((response) => {
            let [firstSlug] = response.slugs;
            let {slug} = firstSlug;

            return slug;
        });
    }
});
    it
} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import Pretender from 'pretender';
import wait from 'ember-test-helpers/wait';
import {createFile, fileUpload} from '../../helpers/file-upload';
import $ from 'jquery';
import run from 'ember-runloop';
import Service from 'ember-service';

const keyCodes = {
    enter: 13
};

const configStub = Service.extend({
    fileStorage: true
});

const notificationsStub = Service.extend({
    showAPIError(error, options) {
        // noop - to be stubbed
    }
});

const sessionStub = Service.extend({
    isAuthenticated: false,
    authorize(authorizer, block) {
        if (this.get('isAuthenticated')) {
            block('Authorization', 'Bearer token');
        }
    }
Exemplo n.º 7
0
import { expect } from 'chai';
import {
    describeComponent,
    it
} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import Service from 'ember-service';
import wait from 'ember-test-helpers/wait';

const featureStub = Service.extend({
    testFlag: true
});

describeComponent(
    'gh-feature-flag',
    'Integration: Component: gh-feature-flag',
    {
        integration: true
    },
    function() {
        let server;

        beforeEach(function () {
            this.register('service:feature', featureStub);
            this.inject.service('feature', {as: 'feature'});
        });

        it('renders properties correctly', function () {
            this.render(hbs`{{gh-feature-flag "testFlag"}}`);
            expect(this.$()).to.have.length(1);
            expect(this.$('label').attr('for')).to.equal(this.$('input[type="checkbox"]').attr('id'));
Exemplo n.º 8
0
/* global md5 */
import Pretender from 'pretender';
import Service from 'ember-service';
import hbs from 'htmlbars-inline-precompile';
import run from 'ember-runloop';
import wait from 'ember-test-helpers/wait';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {timeout} from 'ember-concurrency';

let pathsStub = Service.extend({
    url: {
        api() {
            return '';
        },
        asset(src) {
            return src;
        }
    }
});

const stubKnownGravatar = function (server) {
    server.get('http://www.gravatar.com/avatar/:md5', function () {
        return [200, {'Content-Type': 'image/png'}, ''];
    });

    server.head('http://www.gravatar.com/avatar/:md5', function () {
        return [200, {'Content-Type': 'image/png'}, ''];
    });
};
Exemplo n.º 9
0
export default Service.extend({
  init() {
    let info = localStorage.promptMe;

    if (info) {
      try {
        info = JSON.parse(info);
      } catch (e) {
        info = null;
        console.error("Couldn't parse localStorage.promptMe", e);
      }
    }

    this.set('info', info);
  },

  hasActivePrompt() {
    let info = this.get('info');

    if (!info) { return false; }

    return isStillActive(info);
  },

  getCountdown() {
    let info = this.get('info');

    if (!info) {
      info = this.buildPromptInfo();
    } else if (isStillActive(info)) {
      info.didRefresh = true;
    } else if (didCompletePrompt(info)) {
      info.didComplete = true;
    } else {
      info = this.buildPromptInfo();
    }

    info.prompt = prompts[info.promptIndex];
    info.pastPrompts = this.getPastPrompts();

    return Countdown.create(info);
  },

  getPastPrompts() {
    let promptIndex = getPromptIndex();

    return prompts.slice(promptIndex, prompts.length);
  },

  buildPromptInfo() {
    let timestamp = Date.now();
    let promptIndex = getPromptIndex();

    let promptInfo = { promptIndex, timestamp };

    localStorage.promptMe = JSON.stringify(promptInfo);
    this.set('info', promptInfo);

    return promptInfo;
  }
});
Exemplo n.º 10
0
// Creates a clock service to run intervals.

export default Service.extend({
    second: null,
    minute: null,
    hour:   null,

    init() {
        this.tick();
    },

    tick() {
        let now = moment().utc();

        this.setProperties({
            second: now.seconds(),
            minute: now.minutes(),
            hour:   now.hours()
        });

        if (!testing) {
            run.later(() => {
                this.tick();
            }, ONE_SECOND);
        }

    }

});
/* jshint expr:true */
import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import Service from 'ember-service';
import EmberObject from 'ember-object';
import run from 'ember-runloop';
import DS from 'ember-data';
import wait from 'ember-test-helpers/wait';

const {Errors} = DS;

let configStub = Service.extend({
    blogUrl: 'http://localhost:2368'
});

let mediaQueriesStub = Service.extend({
    maxWidth600: false
});

describe('Integration: Component: gh-tag-settings-form', function () {
    setupComponentTest('gh-tag-settings-form', {
        integration: true
    });

    beforeEach(function () {
        /* eslint-disable camelcase */
        let tag = EmberObject.create({
            id: 1,
            name: 'Test',
Exemplo n.º 12
0
export default Service.extend({
    init() {
        this._super(...arguments);
        this._handlers = [];
        this.loadQueries(MEDIA_QUERIES);
    },

    loadQueries(queries) {
        Object.keys(queries).forEach((key) => {
            this.loadQuery(key, queries[key]);
        });
    },

    loadQuery(key, queryString) {
        let query = window.matchMedia(queryString);

        this.set(key, query.matches);

        let handler = run.bind(this, () => {
            let lastValue = this.get(key);
            let newValue = query.matches;
            if (lastValue !== newValue) {
                this.set(key, query.matches);
            }
        });
        query.addListener(handler);
        this._handlers.push([query, handler]);
    },

    willDestroy() {
        this._handlers.forEach(([query, handler]) => {
            query.removeListener(handler);
        });
        this._super(...arguments);
    }

});
Exemplo n.º 13
0
import $ from 'jquery';
import Pretender from 'pretender';
import Service from 'ember-service';
import hbs from 'htmlbars-inline-precompile';
import run from 'ember-runloop';
import sinon from 'sinon';
import wait from 'ember-test-helpers/wait';
import {UnsupportedMediaTypeError} from 'ghost-admin/services/ajax';
import {createFile, fileUpload} from '../../helpers/file-upload';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';

const notificationsStub = Service.extend({
    showAPIError() {
        // noop - to be stubbed
    }
});

const stubSuccessfulUpload = function (server, delay = 0) {
    server.post('/ghost/api/v0.1/uploads/', function () {
        return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"'];
    }, delay);
};

const stubFailedUpload = function (server, code, error, delay = 0) {
    server.post('/ghost/api/v0.1/uploads/', function () {
        return [code, {'Content-Type': 'application/json'}, JSON.stringify({
            errors: [{
                errorType: error,
                message: `Error: ${error}`
Exemplo n.º 14
0
import Service from 'ember-service';

export default Service.extend({
    content: ''
});
Exemplo n.º 15
0
import Service from 'ember-service';
import Evented from 'ember-evented';
// This is used by the dropdown initializer to manage closing & toggling
import BodyEventListener from 'ghost-admin/mixins/body-event-listener';
import $ from 'jquery';

export default Service.extend(Evented, BodyEventListener, {
    bodyClick(event) {
        let dropdownSelector = '.ember-basic-dropdown-trigger, .ember-basic-dropdown-content';

        if ($(event.target).closest(dropdownSelector).length <= 0) {
            this.closeDropdowns();
        }
    },

    closeDropdowns() {
        this.trigger('close');
    },

    toggleDropdown(dropdownName, dropdownButton) {
        this.trigger('toggle', {target: dropdownName, button: dropdownButton});
    }
});
Exemplo n.º 16
0
export default Service.extend({
  user: null,
  loggedIn: null,
  token: null,
  val: true,

  login(token) {
    let github = new Github({
      token: token,
      auth: "oauth"
    });

    let userPromise = Ember.RSVP.denodeify(github.getUser().show);

    return userPromise("")
    .then((infos) => {
      this.set("user", infos);
      this.set("loggedIn", true);
      this.set("token", token);
      localStorage.setItem("github_token", token);

      return infos;
    });
  },

  restoreSession() {
    let localToken = localStorage.getItem("github_token");
    if(localToken) {
      return this.login(localToken);
    } else {
      return Ember.RSVP.reject();
    }
  },

  deleteSession() {
    console.log('deleting token');
    this.set("user", null);
    this.set("loggedIn", false);
    this.set("token", null);
    localStorage.removeItem('github_token');
  }
});