Merge remote-tracking branch 'origin' into afc-email-tokens

This commit is contained in:
Alberto Fernández Capel 2018-06-05 15:42:17 +01:00
commit 6fb6119ca8
51 changed files with 2388 additions and 446 deletions

View file

@ -10,6 +10,7 @@ GeoIpLookup = require("../../infrastructure/GeoIpLookup")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
UserGetter = require "../User/UserGetter"
FeaturesUpdater = require './FeaturesUpdater'
planFeatures = require './planFeatures'
module.exports = SubscriptionController =
@ -20,6 +21,7 @@ module.exports = SubscriptionController =
viewName = "#{viewName}_#{req.query.v}"
logger.log viewName:viewName, "showing plans page"
currentUser = null
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
return next(err) if err?
render = () ->
@ -29,6 +31,7 @@ module.exports = SubscriptionController =
gaExperiments: Settings.gaExperiments.plansPage
recomendedCurrency:recomendedCurrency
shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
planFeatures: planFeatures
user_id = AuthenticationController.getLoggedInUserId(req)
if user_id?
UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->

View file

@ -0,0 +1,133 @@
module.exports =
[
{
feature: 'number_collab'
value: 'str'
plans: {
free: '1'
coll: '10'
prof: 'unlimited'
}
student: '6'
}
{
feature: 'unlimited_private'
value: 'bool'
info: 'unlimited_private_info'
plans: {
free: true
coll: true
prof: true
},
student: true
}
{
feature: 'realtime_collab'
value: 'bool'
info: 'realtime_collab_info'
plans: {
free: true
coll: true
prof: true
}
student: true
}
{
feature: 'hundreds_templates'
value: 'bool'
info: 'hundreds_templates_info'
plans: {
free: true
coll: true
prof: true
}
student: true
}
{
feature: 'powerful_latex_editor'
value: 'bool'
info: 'latex_editor_info'
plans: {
free: true
coll: true
prof: true
}
student: true
}
{
feature: 'realtime_track_changes'
value: 'bool'
info: 'realtime_track_changes_info'
plans: {
free: false
coll: true
prof: true
},
student: true
}
{
feature: 'reference_search'
value: 'bool'
info: 'reference_search_info'
plans: {
free: false
coll: true
prof: true
},
student: true
},
{
feature: 'reference_sync'
info: 'reference_sync_info'
value: 'bool'
plans: {
free: false
coll: true
prof: true
},
student: true
}
{
feature: 'full_doc_history'
value: 'bool'
info: 'full_doc_history_info'
plans: {
free: false,
coll: true,
prof: true
},
student: true
}
{
feature: 'dropbox_integration_lowercase'
value: 'bool'
info: 'dropbox_integration_info'
plans: {
free: false,
coll: true,
prof: true
},
student: true
},
{
feature: 'github_integration_lowercase'
value: 'bool'
info: 'github_integration_info'
plans: {
free: false,
coll: true,
prof: true
},
student: true
},
{
feature: 'priority_support',
value: 'bool',
plans: {
free: false,
coll: true,
prof: true
},
student: true
},
]

View file

@ -207,6 +207,7 @@ module.exports = class Router
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
webRouter.get "/project/:Project_id/filetree/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2

View file

@ -57,9 +57,12 @@ block content
include ./editor/share
!= moduleIncludes("publish:body", locals)
include ./editor/history/toolbarV2.pug
main#ide-body(
ng-cloak,
role="main",
ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME) }",
layout="main",
ng-hide="state.loading",
resize-on="layout:chat:resize",
@ -70,7 +73,7 @@ block content
)
.ui-layout-west
include ./editor/file-tree
include ./editor/history-file-tree
include ./editor/history/fileTreeV2
.ui-layout-center
include ./editor/editor

View file

@ -1,17 +0,0 @@
aside.file-tree.file-tree-history(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }", ng-show="ui.view == 'history' && history.isV2").full-size
.toolbar.toolbar-filetree
span Modified files
.file-tree-inner
ul.list-unstyled.file-tree-list
li(
ng-repeat="(pathname, doc) in history.selection.docs"
ng-class="{ 'selected': history.selection.pathname == pathname }"
)
.entity
.entity-name.entity-name-history(
ng-click="history.selection.pathname = pathname",
ng-class="{ 'deleted': !!doc.deletedAtV }"
)
i.fa.fa-fw.fa-pencil
span {{ pathname }}

View file

@ -40,90 +40,11 @@ div#history(ng-show="ui.view == 'history'")
p
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
aside.change-list(
ng-controller="HistoryListController"
infinite-scroll="loadMore()"
infinite-scroll-disabled="history.loading || history.atEnd"
infinite-scroll-initialize="ui.view == 'history'"
)
.infinite-scroll-inner
ul.list-unstyled(
ng-class="{\
'hover-state': history.hoveringOverListSelectors\
}"
)
li.change(
ng-repeat="update in history.updates"
ng-class="{\
'first-in-day': update.meta.first_in_day,\
'selected': update.inSelection,\
'selected-to': update.selectedTo,\
'selected-from': update.selectedFrom,\
'hover-selected': update.inHoverSelection,\
'hover-selected-to': update.hoverSelectedTo,\
'hover-selected-from': update.hoverSelectedFrom,\
}"
ng-controller="HistoryListItemController"
)
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
div.selectors
div.range
form
input.selector-from(
type="radio"
name="fromVersion"
ng-model="update.selectedFrom"
ng-value="true"
ng-mouseover="mouseOverSelectedFrom()"
ng-mouseout="mouseOutSelectedFrom()"
ng-show="update.afterSelection || update.inSelection"
)
form
input.selector-to(
type="radio"
name="toVersion"
ng-model="update.selectedTo"
ng-value="true"
ng-mouseover="mouseOverSelectedTo()"
ng-mouseout="mouseOutSelectedTo()"
ng-show="update.beforeSelection || update.inSelection"
)
div.description(ng-click="select()")
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
| Edited
div.docs(ng-repeat="pathname in update.pathnames")
.doc {{ pathname }}
div.docs(ng-repeat="project_op in update.project_ops")
div(ng-if="project_op.rename")
.action Renamed
.doc {{ project_op.rename.pathname }} → {{ project_op.rename.newPathname }}
div(ng-if="project_op.add")
.action Created
.doc {{ project_op.add.pathname }}
div(ng-if="project_op.remove")
.action Deleted
.doc {{ project_op.remove.pathname }}
div.users
div.user(ng-repeat="update_user in update.meta.users")
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)")
.name(ng-if="update_user && update_user.id == user.id") You
.name(ng-if="update_user == null") #{translate("anonymous")}
div.user(ng-if="update.meta.users.length == 0")
.color-square(style="background-color: hsl(100, 100%, 50%)")
span #{translate("anonymous")}
.loading(ng-show="history.loading")
i.fa.fa-spin.fa-refresh
|    #{translate("loading")}...
include ./history/entriesListV1
include ./history/entriesListV2
include ./history/diffPanelV1
include ./history/diffPanelV2
include ./history/previewPanelV2
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
.modal-header

View file

@ -14,7 +14,7 @@
)
| in <strong>{{history.diff.pathname}}</strong>
.toolbar-right(ng-if="permissions.write")
a.btn.btn-danger.btn-sm(
a.btn.btn-danger.btn-xs(
href,
ng-click="openRestoreDiffModal()"
) #{translate("restore_to_before_these_changes")}

View file

@ -0,0 +1,82 @@
aside.change-list(
ng-if="!history.isV2"
ng-controller="HistoryListController"
infinite-scroll="loadMore()"
infinite-scroll-disabled="history.loading || history.atEnd"
infinite-scroll-initialize="ui.view == 'history'"
)
.infinite-scroll-inner
ul.list-unstyled(
ng-class="{\
'hover-state': history.hoveringOverListSelectors\
}"
)
li.change(
ng-repeat="update in history.updates"
ng-class="{\
'first-in-day': update.meta.first_in_day,\
'selected': update.inSelection,\
'selected-to': update.selectedTo,\
'selected-from': update.selectedFrom,\
'hover-selected': update.inHoverSelection,\
'hover-selected-to': update.hoverSelectedTo,\
'hover-selected-from': update.hoverSelectedFrom,\
}"
ng-controller="HistoryListItemController"
)
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
div.selectors
div.range
form
input.selector-from(
type="radio"
name="fromVersion"
ng-model="update.selectedFrom"
ng-value="true"
ng-mouseover="mouseOverSelectedFrom()"
ng-mouseout="mouseOutSelectedFrom()"
ng-show="update.afterSelection || update.inSelection"
)
form
input.selector-to(
type="radio"
name="toVersion"
ng-model="update.selectedTo"
ng-value="true"
ng-mouseover="mouseOverSelectedTo()"
ng-mouseout="mouseOutSelectedTo()"
ng-show="update.beforeSelection || update.inSelection"
)
div.description(ng-click="select()")
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
| #{translate("file_action_edited")}
div.docs(ng-repeat="pathname in update.pathnames")
.doc {{ pathname }}
div.docs(ng-repeat="project_op in update.project_ops")
div(ng-if="project_op.rename")
.action #{translate("file_action_renamed")}
.doc {{ project_op.rename.pathname }} &rarr; {{ project_op.rename.newPathname }}
div(ng-if="project_op.add")
.action #{translate("file_action_created")}
.doc {{ project_op.add.pathname }}
div(ng-if="project_op.remove")
.action #{translate("file_action_deleted")}
.doc {{ project_op.remove.pathname }}
div.users
div.user(ng-repeat="update_user in update.meta.users")
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)")
.name(ng-if="update_user && update_user.id == user.id") You
.name(ng-if="update_user == null") #{translate("anonymous")}
div.user(ng-if="update.meta.users.length == 0")
.color-square(style="background-color: hsl(100, 100%, 50%)")
span #{translate("anonymous")}
.loading(ng-show="history.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...

View file

@ -0,0 +1,174 @@
aside.change-list(
ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
ng-controller="HistoryV2ListController"
)
history-entries-list(
entries="history.updates"
current-user="user"
load-entries="loadMore()"
load-disabled="history.loading || history.atEnd"
load-initialize="ui.view == 'history'"
is-loading="history.loading"
on-entry-select="handleEntrySelect(selectedEntry)"
)
aside.change-list(
ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE"
ng-controller="HistoryListController"
infinite-scroll="loadMore()"
infinite-scroll-disabled="history.loading || history.atEnd"
infinite-scroll-initialize="ui.view == 'history'"
)
.infinite-scroll-inner
ul.list-unstyled(
ng-class="{\
'hover-state': history.hoveringOverListSelectors\
}"
)
li.change(
ng-repeat="update in history.updates"
ng-class="{\
'first-in-day': update.meta.first_in_day,\
'selected': update.inSelection,\
'selected-to': update.selectedTo,\
'selected-from': update.selectedFrom,\
'hover-selected': update.inHoverSelection,\
'hover-selected-to': update.hoverSelectedTo,\
'hover-selected-from': update.hoverSelectedFrom,\
}"
ng-controller="HistoryListItemController"
)
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
div.selectors
div.range
form
input.selector-from(
type="radio"
name="fromVersion"
ng-model="update.selectedFrom"
ng-value="true"
ng-mouseover="mouseOverSelectedFrom()"
ng-mouseout="mouseOutSelectedFrom()"
ng-show="update.afterSelection || update.inSelection"
)
form
input.selector-to(
type="radio"
name="toVersion"
ng-model="update.selectedTo"
ng-value="true"
ng-mouseover="mouseOverSelectedTo()"
ng-mouseout="mouseOutSelectedTo()"
ng-show="update.beforeSelection || update.inSelection"
)
div.description(ng-click="select()")
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
| #{translate("file_action_edited")}
div.docs(ng-repeat="pathname in update.pathnames")
.doc {{ pathname }}
div.docs(ng-repeat="project_op in update.project_ops")
div(ng-if="project_op.rename")
.action #{translate("file_action_renamed")}
.doc {{ project_op.rename.pathname }} &rarr; {{ project_op.rename.newPathname }}
div(ng-if="project_op.add")
.action #{translate("file_action_created")}
.doc {{ project_op.add.pathname }}
div(ng-if="project_op.remove")
.action #{translate("file_action_deleted")}
.doc {{ project_op.remove.pathname }}
div.users
div.user(ng-repeat="update_user in update.meta.users")
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)")
.name(ng-if="update_user && update_user.id == user.id") You
.name(ng-if="update_user == null") #{translate("anonymous")}
div.user(ng-if="update.meta.users.length == 0")
.color-square(style="background-color: hsl(100, 100%, 50%)")
span #{translate("anonymous")}
.loading(ng-show="history.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
script(type="text/ng-template", id="historyEntriesListTpl")
.history-entries(
infinite-scroll="$ctrl.loadEntries()"
infinite-scroll-disabled="$ctrl.loadDisabled"
infinite-scroll-initialize="$ctrl.loadInitialize"
)
.infinite-scroll-inner
history-entry(
ng-repeat="entry in $ctrl.entries"
entry="entry"
current-user="$ctrl.currentUser"
on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })"
ng-show="!$ctrl.isLoading"
)
.loading(ng-show="$ctrl.isLoading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
script(type="text/ng-template", id="historyEntryTpl")
.history-entry(
ng-class="{\
'history-entry-first-in-day': $ctrl.entry.meta.first_in_day,\
'history-entry-selected': $ctrl.entry.inSelection,\
'history-entry-selected-to': $ctrl.entry.selectedTo,\
'history-entry-selected-from': $ctrl.entry.selectedFrom,\
'history-entry-hover-selected': $ctrl.entry.inHoverSelection,\
'history-entry-hover-selected-to': $ctrl.entry.hoverSelectedTo,\
'history-entry-hover-selected-from': $ctrl.entry.hoverSelectedFrom,\
}"
)
time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }}
.history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })")
ol.history-entry-changes
li.history-entry-change(
ng-repeat="pathname in ::$ctrl.entry.pathnames"
)
span.history-entry-change-action #{translate("file_action_edited")}
span.history-entry-change-doc {{ ::pathname }}
li.history-entry-change(
ng-repeat="project_op in ::$ctrl.entry.project_ops"
)
span.history-entry-change-action(
ng-if="::project_op.rename"
) #{translate("file_action_renamed")}
span.history-entry-change-action(
ng-if="::project_op.add"
) #{translate("file_action_created")}
span.history-entry-change-action(
ng-if="::project_op.remove"
) #{translate("file_action_deleted")}
span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }}
.history-entry-metadata
time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }}
span
|
| &bull;
|
ol.history-entry-metadata-users
li.history-entry-metadata-user(ng-repeat="update_user in ::$ctrl.entry.meta.users")
span.name(
ng-if="::update_user && update_user.id != $ctrl.currentUser.id"
ng-style="$ctrl.getUserCSSStyle(update_user);"
) {{ ::$ctrl.displayName(update_user) }}
span.name(
ng-if="::update_user && update_user.id == $ctrl.currentUser.id"
ng-style="$ctrl.getUserCSSStyle(update_user);"
) You
span.name(
ng-if="::update_user == null"
ng-style="$ctrl.getUserCSSStyle(update_user);"
) #{translate("anonymous")}
li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0")
span.name(
ng-style="$ctrl.getUserCSSStyle();"
) #{translate("anonymous")}

View file

@ -0,0 +1,70 @@
aside.file-tree.full-size(
ng-controller="HistoryV2FileTreeController"
ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
)
.history-file-tree-inner
history-file-tree(
file-tree="currentFileTree"
selected-pathname="history.selection.pathname"
on-selected-file-change="handleFileSelection(file)"
is-loading="history.loadingFileTree"
)
aside.file-tree.file-tree-history.full-size(
ng-controller="FileTreeController"
ng-class="{ 'multi-selected': multiSelectedCount > 0 }"
ng-show="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.COMPARE")
.toolbar.toolbar-filetree
span Modified files
.file-tree-inner
ul.list-unstyled.file-tree-list
li(
ng-repeat="(pathname, doc) in history.selection.docs"
ng-class="{ 'selected': history.selection.pathname == pathname }"
)
.entity
.entity-name.entity-name-history(
ng-click="history.selection.pathname = pathname",
ng-class="{ 'deleted': !!doc.deletedAtV }"
)
i.fa.fa-fw.fa-pencil
span {{ pathname }}
script(type="text/ng-template", id="historyFileTreeTpl")
.history-file-tree
history-file-entity(
ng-repeat="fileEntity in $ctrl.fileTree"
file-entity="fileEntity"
ng-show="!$ctrl.isLoading"
)
script(type="text/ng-template", id="historyFileEntityTpl")
.history-file-entity-wrapper
a.history-file-entity-link(
href
ng-click="$ctrl.handleClick()"
ng-class="{ 'history-file-entity-link-selected': $ctrl.isSelected }"
)
span.history-file-entity-name
i.history-file-entity-icon.history-file-entity-icon-folder-state.fa.fa-fw(
ng-class="{\
'fa-chevron-down': ($ctrl.fileEntity.type === 'folder' && $ctrl.isOpen),\
'fa-chevron-right': ($ctrl.fileEntity.type === 'folder' && !$ctrl.isOpen)\
}"
)
i.history-file-entity-icon.fa(
ng-class="$ctrl.iconClass"
)
| {{ ::$ctrl.fileEntity.name }}
div(
ng-show="$ctrl.isOpen"
)
history-file-entity(
ng-repeat="childEntity in $ctrl.fileEntity.children"
file-entity="childEntity"
)

View file

@ -1,4 +1,7 @@
.diff-panel.full-size(ng-if="history.isV2", ng-controller="HistoryV2DiffController")
.diff-panel.full-size(
ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE"
ng-controller="HistoryV2DiffController"
)
.diff(
ng-if="!!history.diff && !history.diff.loading && !history.diff.error",
ng-class="{ 'diff-binary': history.diff.binary }"
@ -16,8 +19,13 @@
}"
)
| in <strong>{{history.diff.pathname}}</strong>
.history-toolbar-btn(
ng-click="toggleHistoryViewMode();"
)
i.fa
| #{translate("view_single_version")}
.toolbar-right(ng-if="history.selection.docs[history.selection.pathname].deletedAtV")
button.btn.btn-danger.btn-sm(
button.btn.btn-danger.btn-xs(
ng-click="restoreDeletedFile()"
ng-show="!restoreState.error"
ng-disabled="restoreState.inflight"
@ -47,4 +55,27 @@
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp;#{translate("loading")}...
.error-panel(ng-show="history.diff.error")
.alert.alert-danger #{translate("generic_something_went_wrong")}
.alert.alert-danger #{translate("generic_something_went_wrong")}
.point-in-time-panel.full-size(
ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
)
.point-in-time-editor-container(
ng-if="!!history.selectedFile && !history.selectedFile.loading && !history.selectedFile.error"
)
.hide-ace-cursor(
ng-if="!history.selectedFile.binary"
ace-editor="history-pointintime",
theme="settings.theme",
font-size="settings.fontSize",
text="history.selectedFile.text",
read-only="true",
resize-on="layout:main:resize",
)
.alert.alert-info(ng-if="history.selectedFile.binary")
| We're still working on showing image and binary changes, sorry. Stay tuned!
.loading-panel(ng-show="history.selectedFile.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp;#{translate("loading")}...
.error-panel(ng-show="history.selectedFile.error")
.alert.alert-danger #{translate("generic_something_went_wrong")}

View file

@ -0,0 +1,13 @@
.history-toolbar(
ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
)
span(ng-show="history.loadingFileTree")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}&nbsp;
time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
.history-toolbar-btn(
ng-click="toggleHistoryViewMode();"
)
i.fa
| #{translate("compare_to_another_version")}

View file

@ -0,0 +1,118 @@
.row
.col-md-12
.page-header.centered.plans-header.text-centered
h1 #{translate("start_x_day_trial", {len:'{{trial_len}}'})}
.row
.col-md-8.col-md-offset-2
p.text-centered #{translate("sl_benefits_plans")}
.row.top-switch
.col-md-6.col-md-offset-3
+plan_switch('card')
.col-md-2.text-right
+currency_dropdown
div(ng-show="showPlans")
.row
.col-md-10.col-md-offset-1
.row
.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
.col-md-4
.card.card-first
.card-header
h2 #{translate("personal")}
.circle #{translate("free")}
+features_free
.col-md-4
.card.card-highlighted
.card-header
h2 #{translate("collaborator")}
.circle
+price_collaborator
+features_collaborator
.col-md-4
.card.card-last
.card-header
h2 #{translate("professional")}
.circle
+price_professional
+features_professional
.card-group.text-centered(ng-if="ui.view == 'student'")
.col-md-4
.card.card-first
.card-header
h2 #{translate("personal")}
.circle #{translate("free")}
+features_free
.col-md-4
.card.card-highlighted
+card_student_monthly
.col-md-4
.card.card-last
+card_student_annual
.row.row-spaced
p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
.row
.col-md-8.col-md-offset-2
.alert.alert-info.text-centered
| #{translate("interested_in_group_licence")}
br
a(href, ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")}
script(type="text/ng-template", id="groupPlanModalTemplate")
.modal-header
h3 #{translate("group_plan_enquiry")}
.modal-body
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()")
span(ng-show="sent == false && error == false")
.form-group
label#title9(for='Field9')
| Name
input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
label#title11.desc(for='Field11')
| Email
.form-group
input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
label#title12.desc(for='Field12')
| University / Company
.form-group
input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
label#title13.desc(for='Field13')
| Position
.form-group
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
.form-group
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
.form-group.text-center
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
span(ng-show="sent == true && error == false")
p Request Sent, Thank you.
span(ng-show="error")
p Error sending request.
.row
.col-md-12
.page-header.plans-header.plans-subheader.text-centered
h2 #{translate("enjoy_these_features")}
.col-md-4
.card.features.text-centered
i.fa.fa-file-text-o.fa-5x
h4 #{translate("unlimited_projects")}
p #{translate("create_unlimited_projects")}
.col-md-4
.card.features.text-centered
i.fa.fa-clock-o.fa-5x
h4 #{translate("full_doc_history")}
p #{translate("never_loose_work")}
.col-md-4
.card.features.text-centered
i.fa.fa-dropbox.fa-5x
| &nbsp; &nbsp;
i.fa.fa-github.fa-5x
h4 #{translate("sync_to_dropbox_and_github")}
p #{translate("access_projects_anywhere")}

View file

@ -0,0 +1,160 @@
.row
.col-md-12
.page-header.centered.plans-header.text-centered
h1.text-capitalize #{translate('instant_access')}
.row
.col-md-8.col-md-offset-2
p.text-centered #{translate("sl_benefits_plans")}
.row.top-switch
.col-md-6.col-md-offset-3
+plan_switch('card')
.col-md-2.text-right
+currency_dropdown
div(ng-show="showPlans")
.row
.col-md-10.col-md-offset-1
.row
.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
.col-md-4
.card.card-first
.card-header
h2 #{translate("personal")}
h5.tagline #{translate("tagline_personal")}
.circle #{translate("free")}
+features_free
.col-md-4
.card.card-highlighted
.best-value
strong #{translate('best_value')}
.card-header
h2 #{translate("collaborator")}
h5.tagline #{translate("tagline_collaborator")}
.circle
+price_collaborator
+features_collaborator
.col-md-4
.card.card-last
.card-header
h2 #{translate("professional")}
h5.tagline #{translate("tagline_professional")}
.circle
+price_professional
+features_professional
.card-group.text-centered(ng-if="ui.view == 'student'")
.col-md-4
.card.card-first
.card-header
h2 #{translate("personal")}
h5.tagline #{translate("tagline_personal")}
.circle #{translate("free")}
+features_free
.col-md-4
.card.card-highlighted
+card_student_annual
.col-md-4
.card.card-last
+card_student_monthly
.row.row-spaced-large.text-centered
i.fa.fa-cc-mastercard.fa-2x &nbsp;
i.fa.fa-cc-visa.fa-2x &nbsp;
i.fa.fa-cc-amex.fa-2x &nbsp;
i.fa.fa-cc-paypal.fa-2x &nbsp;
div.text-centered #{translate('change_plans_any_time')}<br/> #{translate('billed_after_x_days', {len:'{{trial_len}}'})}
.row.row-spaced-large
.col-md-8.col-md-offset-2
.card.text-centered
.card-header
h2 #{translate('looking_multiple_licenses')}
span #{translate('reduce_costs_group_licenses')}
br
br
a.btn.btn-info(href="/i/university/groups") #{translate('find_out_more')}
div
.row.row-spaced-large
.col-sm-12
.page-header.plans-header.plans-subheader.text-centered
h2 #{translate('compare_plan_features')}
.row
.col-md-6.col-md-offset-3
+plan_switch('table')
.col-md-3.text-right
+currency_dropdown
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true")
.col-sm-12(ng-if="ui.view != 'student'")
+table_premium
.col-sm-12(ng-if="ui.view == 'student'")
+table_student
.row.row-spaced-large
.col-md-12
.page-header.plans-header.plans-subheader.text-centered
h2 #{translate('in_good_company')}
.row
.col-md-6
div
.row
.col-md-3
.circle-img
img(src=buildImgPath('advocates/erdogmus.jpg') alt="Professor Erdogmus")
.col-md-9
blockquote
p The ability to track changes and the real-time collaborative nature is what sets ShareLaTeX apart.
footer Professor Erdogmus, Northeastern University
.col-md-6
div
.row
.col-md-3
.circle-img
img(src=buildImgPath('advocates/henderson.jpg') alt="Rob Henderson")
.col-md-9
blockquote
p ShareLaTeX has proven to be a powerful and robust collaboration tool that is widely used in our School.
footer Rob Henderson, School Of Informatics And Computing - Indiana University
.faq
.row.row-spaced-large
.col-md-12
.page-header.plans-header.plans-subheader.text-centered
h2 FAQ
.row
.col-md-6
h3 #{translate("faq_how_free_trial_works_question")}
p #{translate('faq_how_free_trial_works_answer', { len:'{{trial_len}}' })}
.col-md-6
h3 #{translate('faq_change_plans_question')}
p #{translate('faq_change_plans_answer')}
.row
.col-md-6
h3 #{translate('faq_do_collab_need_premium_question')}
p #{translate('faq_do_collab_need_premium_answer')}
.col-md-6
h3 #{translate('faq_need_more_collab_question')}
p !{translate('faq_need_more_collab_answer', { referFriendsLink: '<a href="/user/bonus">' + translate('referring_your_friends') + '</a>'})}
.row
.col-md-6
h3 #{translate('faq_purchase_more_licenses_question')}
p !{translate('faq_purchase_more_licenses_answer', { groupLink: '<a href="/i/university/groups">' + translate('discounted_group_accounts') + '</a>' })}
.col-md-6
h3 #{translate('faq_monthly_or_annual_question')}
p #{translate('faq_monthly_or_annual_answer')}
.row
.col-md-6
h3 #{translate('faq_how_to_pay_question')}
p #{translate('faq_how_to_pay_answer')}
.col-md-6
h3 #{translate('faq_pay_by_invoice_question')}
p !{translate('faq_pay_by_invoice_answer', { groupLink: '<a href="/i/university/groups">' + translate('discounted_group_accounts') + '</a>' })}
.row.row-spaced-large.text-centery
.col-md-12
.plans-header.plans-subheader.text-centered
h2 #{translate('still_have_questions')}
button.btn.btn-info.btn-header.text-capitalize(ng-controller="ContactGeneralModal" ng-click="openModal()") #{translate('get_in_touch')}
!= moduleIncludes("contactModalGeneral", locals)

View file

@ -0,0 +1,162 @@
//- Buy Buttons
mixin btn_buy_collaborator(location)
a.btn.btn-info(
ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}&currency={{currencyCode}}",
ng-click="signUpNowClicked('collaborator','" + location + "')"
)
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
mixin btn_buy_free(location)
a.btn.btn-info(
href="/register"
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
ng-click="signUpNowClicked('free','" + location + "')"
)
span(ng-if="plansVariant !== 'more-details'") #{translate('sign_up_now')}
span.text-capitalize(ng-if="plansVariant === 'more-details'") #{translate('get_started_now')}
mixin btn_buy_professional(location)
a.btn.btn-info(
ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}"
ng-click="signUpNowClicked('professional','" + location + "')"
)
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
mixin btn_buy_student(location, plan)
if plan == 'annual'
a.btn.btn-info(
ng-href="/user/subscription/new?planCode=student-annual&currency={{currencyCode}}",
ng-click="signUpNowClicked('student-annual','" + location + "')"
) #{translate("buy_now")}
else
//- planQueryString will contain _free_trial_7_days
a.btn.btn-info(
ng-href="/user/subscription/new?planCode=student{{planQueryString}}&currency={{currencyCode}}",
ng-click="signUpNowClicked('student-monthly','" + location + "')"
) #{translate("start_free_trial")}
//- Cards
mixin card_student_annual
.best-value(ng-if="plansVariant == 'more-details'")
strong #{translate('best_value')}
.card-header
h2 #{translate("student")} (#{translate("annual")})
h5.tagline(ng-if="plansVariant == 'more-details'") #{translate('tagline_student_annual')}
.circle
span
+price_student_annual
+features_student('card', 'annual')
mixin card_student_monthly
.card-header
h2 #{translate("student")}
h5.tagline(ng-if="plansVariant == 'more-details'") #{translate('tagline_student_monthly')}
.circle
span
+price_student_monthly
+features_student('card', 'monthly')
//- Features Lists
mixin features_collaborator
ul.list-unstyled
li
strong #{translate("collabs_per_proj", {collabcount:10})}
+features_premium
li
br
+btn_buy_collaborator('card')
mixin features_free
ul.list-unstyled
li #{translate("one_collaborator")}
li(class="hidden-xs hidden-sm") &nbsp;
li(class="hidden-xs hidden-sm") &nbsp;
li(class="hidden-xs hidden-sm") &nbsp;
li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'") &nbsp;
li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'") &nbsp;
li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'") &nbsp;
li
br
+btn_buy_free('card')
mixin features_premium
li(ng-if="plansVariant != 'more-details'") #{translate("full_doc_history")}
li(ng-if="plansVariant != 'more-details'") #{translate("sync_to_dropbox")}
li(ng-if="plansVariant != 'more-details'") #{translate("sync_to_github")}
li(ng-if="plansVariant === 'more-details'") &nbsp;
li(ng-if="plansVariant === 'more-details'")
strong #{translate('all_premium_features')}
li(ng-if="plansVariant === 'more-details'") #{translate('sync_dropbox_github')}
li(ng-if="plansVariant === 'more-details'") #{translate('full_doc_history')}
li(ng-if="plansVariant === 'more-details'") #{translate('track_changes')}
li(ng-if="plansVariant === 'more-details'") + #{translate('more').toLowerCase()}
mixin features_professional
ul.list-unstyled
li
strong #{translate("unlimited_collabs")}
+features_premium
li
br
+btn_buy_professional('card')
mixin features_student(location, plan)
ul.list-unstyled
li
strong #{translate("collabs_per_proj", {collabcount:6})}
+features_premium
li
br
+btn_buy_student(location, plan)
//- Prices
mixin price_collaborator
span(ng-if="ui.view == 'monthly'")
| {{plans[currencyCode]['collaborator']['monthly']}}
span.small /mo
span(ng-if="ui.view == 'annual'")
| {{plans[currencyCode]['collaborator']['annual']}}
span.small /yr
mixin price_professional
span(ng-if="ui.view == 'monthly'")
| {{plans[currencyCode]['professional']['monthly']}}
span.small /mo
span(ng-if="ui.view == 'annual'")
| {{plans[currencyCode]['professional']['annual']}}
span.small /yr
mixin price_student_annual
| {{plans[currencyCode]['student']['annual']}}
span.small /yr
mixin price_student_monthly
| {{plans[currencyCode]['student']['monthly']}}
span.small /mo
//- UI Control
mixin currency_dropdown
.dropdown.currency-dropdown(dropdown)
a.btn.btn-default.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
span.caret
ul.dropdown-menu.dropdown-menu-right.text-right(role="menu")
li(ng-repeat="(currency, value) in plans")
a(
href="#",
ng-click="changeCurreny($event, currency)"
) {{currency}} ({{value['symbol']}})
mixin plan_switch(location)
ul.nav.nav-pills
li(ng-class="{'active': ui.view == 'monthly'}")
a(
href="#"
ng-click="switchToMonthly($event,'" + location + "')"
) #{translate("monthly")}
li(ng-class="{'active': ui.view == 'annual'}")
a(
href="#"
ng-click="switchToAnnual($event,'" + location + "')"
) #{translate("annual")}
li(ng-class="{'active': ui.view == 'student'}")
a(
href="#"
ng-click="switchToStudent($event,'" + location + "')"
) #{translate("half_price_student")}

View file

@ -0,0 +1,107 @@
//- Features Tables
mixin table_premium
table.card.plans-table
tr
th
th #{translate("personal")}
th #{translate("collaborator")}
.outer.outer-top
.outer-content
.best-value
strong #{translate('best_value')}
th #{translate("professional")}
tr
td #{translate("price")}
td #{translate("free")}
td
+price_collaborator
td
+price_professional
for feature in planFeatures
tr
td(event-tracking="features-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}-exp-{{plansVariant}}`)
if feature.info
span(tooltip=translate(feature.info)) #{translate(feature.feature)}
else
| #{translate(feature.feature)}
for plan in feature.plans
td
if feature.value == 'str'
| #{plan}
else if plan
i.fa.fa-check
else
i.fa.fa-times
tr
td
td
+btn_buy_free('table')
td
+btn_buy_collaborator('table')
.outer.outer-btm
.outer-content &nbsp;
td
+btn_buy_professional('table')
mixin table_cell_student(feature)
if feature.value == 'str'
| #{feature.student}
else if feature.student
i.fa.fa-check
else
i.fa.fa-times
mixin table_student
table.card.plans-table
tr
th
th #{translate("personal")}
th #{translate("student")} (#{translate("annual")})
.outer.outer-top
.outer-content
.best-value
strong Best Value
th #{translate("student")}
tr
td #{translate("price")}
td #{translate("free")}
td
+price_student_annual
td
+price_student_monthly
for feature in planFeatures
tr
td(event-tracking="plans-page-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}-exp-{{plansVariant}}`)
if feature.info
span(tooltip=translate(feature.info)) #{translate(feature.feature)}
else
| #{translate(feature.feature)}
td
if feature.value == 'str'
| #{feature.plans.free}
else if feature.plans.free
i.fa.fa-check
else
i.fa.fa-times
td
+table_cell_student(feature)
td
+table_cell_student(feature)
tr
td
td
+btn_buy_free('table')
td
+btn_buy_student('table', 'annual')
.outer.outer-btm
.outer-content &nbsp;
td
+btn_buy_student('table', 'monthly')

View file

@ -31,7 +31,10 @@ block content
li(ng-repeat="(currency, value) in plans")
a(
ng-click="changeCurrency(currency)",
) {{currency}} ({{value['symbol']}})
) {{currency}} ({{value['symbol']}})
.row(ng-if="plansVariant == 'more-details' && planCode == 'student-annual' || plansVariant == 'more-details' && planCode == 'student-monthly'")
.col-xs-12
p.student-disclaimer #{translate('student_disclaimer')}
hr.thin
.row
.col-md-12.text-center

View file

@ -1,253 +1,18 @@
extends ../layout
include _plans_page_mixins
include _plans_page_tables
block scripts
script(type='text/javascript').
window.recomendedCurrency = '#{recomendedCurrency}'
window.abCurrencyFlag = '#{abCurrencyFlag}'
window.shouldABTestPlans = #{shouldABTestPlans || false}
script(type='text/javascript').
(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true;
s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket/api.js';
var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})();
block content
.content.content-alt
.content.plans(ng-controller="PlansController")
.container
.row
.col-md-12
.page-header.centered.plans-header.text-centered
h1(ng-cloak) #{translate("start_x_day_trial", {len:'{{trial_len}}'})}
.row
.col-md-8.col-md-offset-2
p.text-centered #{translate("sl_benefits_plans")}
.row(ng-cloak)
.col-md-6.col-md-offset-3
ul.nav.nav-pills
li(ng-class="{'active': ui.view == 'monthly'}")
a(
href,
ng-click="switchToMonthly()"
) #{translate("monthly")}
li(ng-class="{'active': ui.view == 'annual'}")
a(
href
ng-click="switchToAnnual()"
) #{translate("annual")}
li(ng-class="{'active': ui.view == 'student'}")
a(
href,
ng-click="switchToStudent()"
) #{translate("half_price_student")}
.col-md-2.text-right
.dropdown.currency-dropdown(dropdown)
a.btn.btn-default.dropdown-toggle#currenyDropdown(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
span.caret
ul.dropdown-menu.dropdown-menu-right.text-right(role="menu")
li(ng-repeat="(currency, value) in plans")
a(
href,
ng-click="changeCurreny(currency)"
) {{currency}} ({{value['symbol']}})
div(ng-show="showPlans")
.row(ng-cloak)
.col-md-10.col-md-offset-1
.row
.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
.col-md-4
.card.card-first
.card-header
h2 #{translate("personal")}
.circle #{translate("free")}
ul.list-unstyled
li #{translate("one_collaborator")}
li &nbsp;
li &nbsp;
li &nbsp;
li
br
a.btn.btn-info(
href="/register"
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
) #{translate("sign_up_now")}
.col-md-4
.card.card-highlighted
.card-header
h2 #{translate("collaborator")}
.circle
span(ng-if="ui.view == 'monthly'")
| {{plans[currencyCode]['collaborator']['monthly']}}
span.small /mo
span(ng-if="ui.view == 'annual'")
| {{plans[currencyCode]['collaborator']['annual']}}
span.small /yr
ul.list-unstyled
li
strong #{translate("collabs_per_proj", {collabcount:10})}
li #{translate("full_doc_history")}
li #{translate("sync_to_dropbox")}
li #{translate("sync_to_github")}
li
br
a.btn.btn-info(
ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}&currency={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
)
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
.col-md-4
.card.card-last
.card-header
h2 #{translate("professional")}
.circle
span(ng-if="ui.view == 'monthly'")
| {{plans[currencyCode]['professional']['monthly']}}
span.small /mo
span(ng-if="ui.view == 'annual'")
| {{plans[currencyCode]['professional']['annual']}}
span.small /yr
ul.list-unstyled
li
strong #{translate("unlimited_collabs")}
li #{translate("full_doc_history")}
li #{translate("sync_to_dropbox")}
li #{translate("sync_to_github")}
li
br
a.btn.btn-info(
ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('professional')"
)
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
.card-group.text-centered(ng-if="ui.view == 'student'")
.col-md-4
.card.card-first
.card-header
h2 #{translate("personal")}
.circle #{translate("free")}
ul.list-unstyled
li #{translate("one_collaborator")}
li &nbsp;
li &nbsp;
li &nbsp;
li
br
a.btn.btn-info(
href="/register"
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
) #{translate("sign_up_now")}
.col-md-4
.card.card-highlighted
.card-header
h2 #{translate("student")}
.circle
span
| {{plans[currencyCode]['student']['monthly']}}
span.small /mo
ul.list-unstyled
li
strong #{translate("collabs_per_proj", {collabcount:6})}
li #{translate("full_doc_history")}
li #{translate("sync_to_dropbox")}
li #{translate("sync_to_github")}
li
br
a.btn.btn-info(
ng-href="/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}&currency={{currencyCode}}",
ng-click="signUpNowClicked('student-monthly')"
) #{translate("start_free_trial")}
.col-md-4
.card.card-last
.card-header
h2 #{translate("student")} (#{translate("annual")})
.circle
span
| {{plans[currencyCode]['student']['annual']}}
span.small /yr
ul.list-unstyled
li
strong #{translate("collabs_per_proj", {collabcount:6})}
li #{translate("full_doc_history")}
li #{translate("sync_to_dropbox")}
li #{translate("sync_to_github")}
li
br
a.btn.btn-info(
ng-href="/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}&currency={{currencyCode}}",
ng-click="signUpNowClicked('student-annual')"
) #{translate("buy_now")}
.row.row-spaced(ng-cloak)
p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
.row(ng-cloak)
.col-md-8.col-md-offset-2
.alert.alert-info.text-centered
| #{translate("interested_in_group_licence")}
br
a(href, ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")}
script(type="text/ng-template", id="groupPlanModalTemplate")
.modal-header
h3 #{translate("group_plan_enquiry")}
.modal-body
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
span(ng-show="sent == false && error == false")
.form-group
label#title9(for='Field9')
| Name
input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
label#title11.desc(for='Field11')
| Email
.form-group
input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
label#title12.desc(for='Field12')
| University / Company
.form-group
input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
label#title13.desc(for='Field13')
| Position
.form-group
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
.form-group
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
.form-group.text-center
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
span(ng-show="sent == true && error == false")
p Request Sent, Thank you.
span(ng-show="error")
p Error sending request.
.row
.col-md-12
.page-header.plans-header.plans-subheader.text-centered
h2 #{translate("enjoy_these_features")}
.col-md-4
.card.features.text-centered
i.fa.fa-file-text-o.fa-5x
h4 #{translate("unlimited_projects")}
p #{translate("create_unlimited_projects")}
.col-md-4
.card.features.text-centered
i.fa.fa-clock-o.fa-5x
h4 #{translate("full_doc_history")}
p #{translate("never_loose_work")}
.col-md-4
.card.features.text-centered
i.fa.fa-dropbox.fa-5x
| &nbsp; &nbsp;
i.fa.fa-github.fa-5x
h4 #{translate("sync_to_dropbox_and_github")}
p #{translate("access_projects_anywhere")}
.container(class="more-details" ng-cloak ng-if="plansVariant === 'more-details'")
include _plans_page_details_more
.container(ng-cloak ng-if="plansVariant != 'more-details'")
include _plans_page_details_less

View file

@ -4,11 +4,23 @@
# event not sent to MB.
# for MB, add event-tracking-mb='true'
# by default, event sent to MB via sendMB
# this can be changed to use sendMBOnce via event-tracking-send-once='true' attribute
# event not sent to GA.
# for GA, add event-tracking-ga attribute, where the value is the GA category
# Either GA or MB can use the attribute event-tracking-send-once='true' to
# send event just once
# MB will use the key and GA will use the action to determine if the event
# has been sent
# event-tracking-trigger attribute is required to send event
isInViewport = (element) ->
elTop = element.offset().top
elBtm = elTop + element.outerHeight()
viewportTop = $(window).scrollTop()
viewportBtm = viewportTop + $(window).height()
elBtm > viewportTop && elTop < viewportBtm
define [
'base'
], (App) ->
@ -22,20 +34,42 @@ define [
sendGA = attrs.eventTrackingGa || false
sendMB = attrs.eventTrackingMb || false
sendMBFunction = if attrs.eventTrackingSendOnce then 'sendMBOnce' else 'sendMB'
sendGAFunction = if attrs.eventTrackingSendOnce then 'sendGAOnce' else 'send'
segmentation = scope.eventSegmentation || {}
segmentation.page = window.location.pathname
sendEvent = () ->
sendEvent = (scrollEvent) ->
###
@param {boolean} scrollEvent Use to unbind scroll event
###
if sendMB
event_tracking[sendMBFunction] scope.eventTracking, segmentation
if sendGA
event_tracking.send attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
event_tracking[sendGAFunction] attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
if scrollEvent
$(window).unbind('resize scroll')
if attrs.eventTrackingTrigger == 'load'
sendEvent()
else if attrs.eventTrackingTrigger == 'click'
element.on 'click', (e) ->
sendEvent()
else if attrs.eventTrackingTrigger == 'hover'
timer = null
timeoutAmt = 500
if attrs.eventHoverAmt
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
element.on 'mouseenter', () ->
timer = setTimeout((-> sendEvent()), timeoutAmt)
return
.on 'mouseleave', () ->
clearTimeout(timer)
else if attrs.eventTrackingTrigger == 'scroll'
if !event_tracking.eventInCache(scope.eventTracking)
$(window).on 'resize scroll', () ->
_.throttle(
if isInViewport(element) && !event_tracking.eventInCache(scope.eventTracking)
sendEvent(true)
, 500)
}
]
]

View file

@ -335,6 +335,11 @@ define [
return null
projectContainsFolder: () ->
for entity in @$scope.rootFolder.children
return true if entity.type == 'folder'
return false
existsInThisFolder: (folder, name) ->
for entity in folder?.children or []
return true if entity.name is name

View file

@ -1,6 +1,7 @@
define [
"base"
], (App) ->
"ide/file-tree/util/iconTypeFromName"
], (App, iconTypeFromName) ->
App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
$scope.select = (e) ->
if e.ctrlKey or e.metaKey
@ -70,18 +71,7 @@ define [
$scope.$on "delete:selected", () ->
$scope.openDeleteModal() if $scope.entity.selected
$scope.iconTypeFromName = (name) ->
ext = name.split(".").pop()?.toLowerCase()
if ext in ["png", "pdf", "jpg", "jpeg", "gif"]
return "image"
else if ext in ["csv", "xls", "xlsx"]
return "table"
else if ext in ["py", "r"]
return "file-text"
else if ext in ['bib']
return 'book'
else
return "file"
$scope.iconTypeFromName = iconTypeFromName
]
App.controller "DeleteEntityModalController", [

View file

@ -0,0 +1,13 @@
define [], () ->
return iconTypeFromName = (name) ->
ext = name.split(".").pop()?.toLowerCase()
if ext in ["png", "pdf", "jpg", "jpeg", "gif"]
return "image"
else if ext in ["csv", "xls", "xlsx"]
return "table"
else if ext in ["py", "r"]
return "file-text"
else if ext in ['bib']
return 'book'
else
return "file"

View file

@ -4,7 +4,6 @@ define [
"ide/history/util/displayNameForUser"
"ide/history/controllers/HistoryListController"
"ide/history/controllers/HistoryDiffController"
"ide/history/controllers/HistoryV2DiffController"
"ide/history/directives/infiniteScroll"
], (moment, ColorManager, displayNameForUser) ->
class HistoryManager

View file

@ -2,13 +2,20 @@ define [
"moment"
"ide/colors/ColorManager"
"ide/history/util/displayNameForUser"
"ide/history/controllers/HistoryListController"
"ide/history/controllers/HistoryDiffController"
"ide/history/util/HistoryViewModes"
"ide/history/controllers/HistoryV2ListController"
"ide/history/controllers/HistoryV2DiffController"
"ide/history/controllers/HistoryV2FileTreeController"
"ide/history/directives/infiniteScroll"
], (moment, ColorManager, displayNameForUser) ->
"ide/history/components/historyEntriesList"
"ide/history/components/historyEntry"
"ide/history/components/historyFileTree"
"ide/history/components/historyFileEntity"
], (moment, ColorManager, displayNameForUser, HistoryViewModes) ->
class HistoryManager
constructor: (@ide, @$scope) ->
@reset()
@$scope.HistoryViewModes = HistoryViewModes
@$scope.toggleHistory = () =>
if @$scope.ui.view == "history"
@ -16,17 +23,31 @@ define [
else
@show()
@$scope.$watch "history.selection.updates", (updates) =>
if updates? and updates.length > 0
@_selectDocFromUpdates()
@reloadDiff()
@$scope.toggleHistoryViewMode = () =>
if @$scope.history.viewMode == HistoryViewModes.COMPARE
@reset()
@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
else
@reset()
@$scope.history.viewMode = HistoryViewModes.COMPARE
@$scope.$watch "history.selection.pathname", () =>
@reloadDiff()
@$scope.$watch "history.selection.updates", (updates) =>
if @$scope.history.viewMode == HistoryViewModes.COMPARE
if updates? and updates.length > 0
@_selectDocFromUpdates()
@reloadDiff()
@$scope.$watch "history.selection.pathname", (pathname) =>
if @$scope.history.viewMode == HistoryViewModes.POINT_IN_TIME
if pathname?
@loadFileAtPointInTime()
else
@reloadDiff()
show: () ->
@$scope.ui.view = "history"
@reset()
@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
hide: () ->
@$scope.ui.view = "editor"
@ -35,6 +56,7 @@ define [
@$scope.history = {
isV2: true
updates: []
viewMode: null
nextBeforeTimestamp: null
atEnd: false
selection: {
@ -46,16 +68,33 @@ define [
toV: null
}
}
diff: null
files: []
diff: null # When history.viewMode == HistoryViewModes.COMPARE
selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME
}
restoreFile: (version, pathname) ->
url = "/project/#{@$scope.project_id}/restore_file"
@ide.$http.post(url, {
version, pathname,
_csrf: window.csrfToken
})
loadFileTreeForUpdate: (update) ->
{fromV, toV} = update
url = "/project/#{@$scope.project_id}/filetree/diff"
query = [ "from=#{toV}", "to=#{toV}" ]
url += "?" + query.join("&")
@$scope.history.loadingFileTree = true
@$scope.history.selectedFile = null
@$scope.history.selection.pathname = null
@ide.$http
.get(url)
.then (response) =>
@$scope.history.files = response.data.diff
@$scope.history.loadingFileTree = false
MAX_RECENT_UPDATES_TO_SELECT: 5
autoSelectRecentUpdates: () ->
return if @$scope.history.updates.length == 0
@ -70,12 +109,28 @@ define [
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
autoSelectLastUpdate: () ->
return if @$scope.history.updates.length == 0
@selectUpdate @$scope.history.updates[0]
selectUpdate: (update) ->
selectedUpdateIndex = @$scope.history.updates.indexOf update
if selectedUpdateIndex == -1
selectedUpdateIndex = 0
for update in @$scope.history.updates
update.selectedTo = false
update.selectedFrom = false
@$scope.history.updates[selectedUpdateIndex].selectedTo = true
@$scope.history.updates[selectedUpdateIndex].selectedFrom = true
@loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex]
BATCH_SIZE: 10
fetchNextBatchOfUpdates: () ->
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
if @$scope.history.nextBeforeTimestamp?
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
@$scope.history.loading = true
@$scope.history.loadingFileTree = true
@ide.$http
.get(url)
.then (response) =>
@ -86,6 +141,23 @@ define [
@$scope.history.atEnd = true
@$scope.history.loading = false
loadFileAtPointInTime: () ->
pathname = @$scope.history.selection.pathname
toV = @$scope.history.selection.updates[0].toV
url = "/project/#{@$scope.project_id}/diff"
query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"]
url += "?" + query.join("&")
@$scope.history.selectedFile =
loading: true
@ide.$http
.get(url)
.then (response) =>
{text, binary} = @_parseDiff(response.data.diff)
@$scope.history.selectedFile.binary = binary
@$scope.history.selectedFile.text = text
@$scope.history.selectedFile.loading = false
.catch () ->
reloadDiff: () ->
diff = @$scope.history.diff
{updates} = @$scope.history.selection
@ -200,7 +272,11 @@ define [
@$scope.history.updates =
@$scope.history.updates.concat(updates)
@autoSelectRecentUpdates() if firstLoad
if firstLoad
if @$scope.history.viewMode == HistoryViewModes.COMPARE
@autoSelectRecentUpdates()
else
@autoSelectLastUpdate()
_perDocSummaryOfUpdates: (updates) ->
# Track current_pathname -> original_pathname

View file

@ -0,0 +1,19 @@
define [
"base"
], (App) ->
historyEntriesListController = ($scope, $element, $attrs) ->
ctrl = @
return
App.component "historyEntriesList", {
bindings:
entries: "<"
loadEntries: "&"
loadDisabled: "<"
loadInitialize: "<"
isLoading: "<"
currentUser: "<"
onEntrySelect: "&"
controller: historyEntriesListController
templateUrl: "historyEntriesListTpl"
}

View file

@ -0,0 +1,27 @@
define [
"base"
"ide/history/util/displayNameForUser"
], (App, displayNameForUser) ->
historyEntryController = ($scope, $element, $attrs) ->
ctrl = @
ctrl.displayName = displayNameForUser
ctrl.getProjectOpDoc = (projectOp) ->
if projectOp.rename? then "#{ projectOp.rename.pathname}#{ projectOp.rename.newPathname }"
else if projectOp.add? then "#{ projectOp.add.pathname}"
else if projectOp.remove? then "#{ projectOp.remove.pathname}"
ctrl.getUserCSSStyle = (user) ->
hue = user?.hue or 100
if ctrl.entry.inSelection
color : "#FFF"
else
color: "hsl(#{ hue }, 70%, 50%)"
return
App.component "historyEntry", {
bindings:
entry: "<"
currentUser: "<"
onSelect: "&"
controller: historyEntryController
templateUrl: "historyEntryTpl"
}

View file

@ -0,0 +1,34 @@
define [
"base"
"ide/file-tree/util/iconTypeFromName"
], (App, iconTypeFromName) ->
# TODO Add arrows in folders
historyFileEntityController = ($scope, $element, $attrs) ->
ctrl = @
_handleFolderClick = () ->
ctrl.isOpen = !ctrl.isOpen
ctrl.iconClass = _getFolderIcon()
_handleFileClick = () ->
ctrl.historyFileTreeController.handleEntityClick ctrl.fileEntity
_getFolderIcon = () ->
if ctrl.isOpen then "fa-folder-open" else "fa-folder"
ctrl.$onInit = () ->
if ctrl.fileEntity.type == "folder"
ctrl.isOpen = true
ctrl.iconClass = _getFolderIcon()
ctrl.handleClick = _handleFolderClick
else
ctrl.iconClass = "fa-#{ iconTypeFromName(ctrl.fileEntity.name) }"
ctrl.handleClick = _handleFileClick
$scope.$watch (() -> ctrl.historyFileTreeController.selectedPathname), (newPathname) ->
ctrl.isSelected = ctrl.fileEntity.pathname == newPathname
return
App.component "historyFileEntity", {
require:
historyFileTreeController: "^historyFileTree"
bindings:
fileEntity: "<"
controller: historyFileEntityController
templateUrl: "historyFileEntityTpl"
}

View file

@ -0,0 +1,18 @@
define [
"base"
], (App) ->
historyFileTreeController = ($scope, $element, $attrs) ->
ctrl = @
ctrl.handleEntityClick = (file) ->
ctrl.onSelectedFileChange file: file
return
App.component "historyFileTree", {
bindings:
fileTree: "<"
selectedPathname: "<"
onSelectedFileChange: "&"
isLoading: "<"
controller: historyFileTreeController
templateUrl: "historyFileTreeTpl"
}

View file

@ -5,7 +5,7 @@ define [
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
$scope.hoveringOverListSelectors = false
$scope.loadMore = () =>
ide.historyManager.fetchNextBatchOfUpdates()

View file

@ -0,0 +1,54 @@
define [
"base"
], (App) ->
App.controller "HistoryV2FileTreeController", ["$scope", "ide", "_", ($scope, ide, _) ->
_previouslySelectedPathname = null
$scope.currentFileTree = []
_pathnameExistsInFiles = (pathname, files) ->
_.any files, (file) -> file.pathname == pathname
_getSelectedDefaultPathname = (files) ->
selectedPathname = null
if _previouslySelectedPathname? and _pathnameExistsInFiles _previouslySelectedPathname, files
selectedPathname = _previouslySelectedPathname
else
mainFile = _.find files, (file) -> /main\.tex$/.test file.pathname
if mainFile?
selectedPathname = _previouslySelectedPathname = mainFile.pathname
else
selectedPathname = _previouslySelectedPathname = files[0].pathname
return selectedPathname
$scope.handleFileSelection = (file) ->
$scope.history.selection.pathname = _previouslySelectedPathname = file.pathname
$scope.$watch 'history.files', (files) ->
if files? and files.length > 0
$scope.currentFileTree = _.reduce files, _reducePathsToTree, []
$scope.history.selection.pathname = _getSelectedDefaultPathname(files)
_reducePathsToTree = (currentFileTree, fileObject) ->
filePathParts = fileObject.pathname.split "/"
currentFileTreeLocation = currentFileTree
for pathPart, index in filePathParts
isFile = index == filePathParts.length - 1
if isFile
fileTreeEntity =
name: pathPart
pathname: fileObject.pathname
type: "file"
operation: fileObject.operation || "edited"
currentFileTreeLocation.push fileTreeEntity
else
fileTreeEntity = _.find currentFileTreeLocation, (entity) => entity.name == pathPart
if !fileTreeEntity?
fileTreeEntity =
name: pathPart
type: "folder"
children: []
currentFileTreeLocation.push fileTreeEntity
currentFileTreeLocation = fileTreeEntity.children
return currentFileTree
]

View file

@ -0,0 +1,76 @@
define [
"base",
"ide/history/util/displayNameForUser"
], (App, displayNameForUser) ->
App.controller "HistoryV2ListController", ["$scope", "ide", ($scope, ide) ->
$scope.hoveringOverListSelectors = false
$scope.loadMore = () =>
ide.historyManager.fetchNextBatchOfUpdates()
$scope.handleEntrySelect = (entry) ->
# $scope.$applyAsync () ->
ide.historyManager.selectUpdate(entry)
$scope.recalculateSelectedUpdates()
$scope.recalculateSelectedUpdates = () ->
beforeSelection = true
afterSelection = false
$scope.history.selection.updates = []
for update in $scope.history.updates
if update.selectedTo
inSelection = true
beforeSelection = false
update.beforeSelection = beforeSelection
update.inSelection = inSelection
update.afterSelection = afterSelection
if inSelection
$scope.history.selection.updates.push update
if update.selectedFrom
inSelection = false
afterSelection = true
$scope.recalculateHoveredUpdates = () ->
hoverSelectedFrom = false
hoverSelectedTo = false
for update in $scope.history.updates
# Figure out whether the to or from selector is hovered over
if update.hoverSelectedFrom
hoverSelectedFrom = true
if update.hoverSelectedTo
hoverSelectedTo = true
if hoverSelectedFrom
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
inHoverSelection = false
for update in $scope.history.updates
if update.selectedTo
update.hoverSelectedTo = true
inHoverSelection = true
update.inHoverSelection = inHoverSelection
if update.hoverSelectedFrom
inHoverSelection = false
if hoverSelectedTo
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
inHoverSelection = false
for update in $scope.history.updates
if update.hoverSelectedTo
inHoverSelection = true
update.inHoverSelection = inHoverSelection
if update.selectedFrom
update.hoverSelectedFrom = true
inHoverSelection = false
$scope.resetHoverState = () ->
for update in $scope.history.updates
delete update.hoverSelectedFrom
delete update.hoverSelectedTo
delete update.inHoverSelection
$scope.$watch "history.updates.length", () ->
$scope.recalculateSelectedUpdates()
]

View file

@ -0,0 +1,4 @@
define [], () ->
HistoryViewModes =
POINT_IN_TIME : 'point_in_time'
COMPARE : 'compare'

View file

@ -50,6 +50,10 @@ define [
send: (category, action, label, value)->
ga('send', 'event', category, action, label, value)
sendGAOnce: (category, action, label, value) ->
if ! _eventInCache(action)
_addEventToCache(action)
@send category, action, label, value
editingSessionHeartbeat: () ->
return unless nextHeartbeat <= new Date()
@ -86,6 +90,9 @@ define [
if ! _eventInCache(key)
_addEventToCache(key)
@sendMB key, segmentation
eventInCache: (key) ->
_eventInCache(key)
}

View file

@ -9,6 +9,7 @@ define [
$scope.currencyCode = MultiCurrencyPricing.currencyCode
$scope.plans = MultiCurrencyPricing.plans
$scope.planCode = window.plan_code
$scope.switchToStudent = ()->
currentPlanCode = window.plan_code
@ -234,3 +235,6 @@ define [
{code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'},
{code:'ZM',name:'Zambia'},{code:'AX',name:'&angst;land Islandscode:'}
]
sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)->
$scope.plansVariant = chosenVariation

View file

@ -3,7 +3,6 @@ define [
"libs/recurly-4.8.5"
], (App, recurly) ->
App.factory "MultiCurrencyPricing", () ->
currencyCode = window.recomendedCurrency
@ -146,17 +145,16 @@ define [
}
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack) ->
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter) ->
$scope.showPlans = false
$scope.plansVariant = 'default'
$scope.shouldABTestPlans = window.shouldABTestPlans
if $scope.shouldABTestPlans
$scope.showPlans = true
else
$scope.showPlans = true
sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)->
$scope.plansVariant = chosenVariation
$scope.showPlans = true
$scope.plans = MultiCurrencyPricing.plans
@ -169,44 +167,57 @@ define [
$scope.ui =
view: "monthly"
$scope.changeCurreny = (newCurrency)->
$scope.changeCurreny = (e, newCurrency)->
e.preventDefault()
$scope.currencyCode = newCurrency
# because ternary logic in angular bindings is hard
$scope.getCollaboratorPlanCode = () ->
view = $scope.ui.view
variant = $scope.plansVariant
if view == "annual"
if variant == "default"
return "collaborator-annual"
else
return "collaborator-annual_#{variant}"
return "collaborator-annual"
else
if variant == "default"
return "collaborator#{$scope.planQueryString}"
else
return "collaborator_#{variant}"
return "collaborator#{$scope.planQueryString}"
$scope.signUpNowClicked = (plan, annual)->
event_tracking.sendMB 'plans-page-start-trial', {plan}
$scope.signUpNowClicked = (plan, location)->
if $scope.ui.view == "annual"
plan = "#{plan}_annual"
event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
plan = eventLabel(plan, location)
event_tracking.sendMB 'plans-page-start-trial', {plan}
event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
if $scope.shouldABTestPlans
sixpack.convert 'plans-details'
$scope.switchToMonthly = ->
$scope.ui.view = "monthly"
event_tracking.send 'subscription-funnel', 'plans-page', 'monthly-prices'
$scope.switchToMonthly = (e, location) ->
uiView = 'monthly'
switchEvent(e, uiView + '-prices', location)
$scope.ui.view = uiView
$scope.switchToStudent = ->
$scope.ui.view = "student"
event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
$scope.switchToStudent = (e, location) ->
uiView = 'student'
switchEvent(e, uiView + '-prices', location)
$scope.ui.view = uiView
$scope.switchToAnnual = ->
$scope.ui.view = "annual"
event_tracking.send 'subscription-funnel', 'plans-page', 'annual-prices'
$scope.switchToAnnual = (e, location) ->
uiView = 'annual'
switchEvent(e, uiView + '-prices', location)
$scope.ui.view = uiView
$scope.openGroupPlanModal = () ->
$modal.open {
templateUrl: "groupPlanModalTemplate"
}
event_tracking.send 'subscription-funnel', 'plans-page', 'group-inquiry-potential'
eventLabel = (label, location) ->
if location && $scope.plansVariant != 'default'
label = label + '-' + location
if $scope.plansVariant != 'default'
label += '-exp-' + $scope.plansVariant
label
switchEvent = (e, label, location) ->
e.preventDefault()
gaLabel = eventLabel(label, location)
event_tracking.send 'subscription-funnel', 'plans-page', gaLabel

View file

@ -425,7 +425,7 @@ define [
for projectId in projectIds
queuedHttp {
method: "POST"
url: "/project/#{project_id}/restore"
url: "/project/#{projectId}/restore"
headers:
"X-CSRF-Token": window.csrfToken
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -80,6 +80,7 @@
@import "app/review-features-page.less";
@import "app/error-pages.less";
@import "app/v1-badge.less";
@import "app/editor/history-v2.less";
@import "app/metrics.less";
// Vendor CSS

View file

@ -74,9 +74,13 @@
#ide-body {
.full-size;
top: 40px;
top: @ide-body-top-offset;
&.ide-history-open {
top: @ide-body-top-offset + @editor-toolbar-height;
}
}
#editor, #editor-rich-text {
.full-size;
}

View file

@ -0,0 +1,498 @@
.history-toolbar {
display: flex;
align-items: center;
position: absolute;
width: 100%;
top: @ide-body-top-offset;
height: @editor-toolbar-height;
line-height: 1;
font-size: @font-size-small;
background-color: @history-toolbar-bg-color;
z-index: 1;
color: @history-toolbar-color;
padding-left: (@line-height-computed / 2);
}
.history-toolbar when (@is-overleaf = false) {
border-bottom: @toolbar-border-bottom;
}
.history-toolbar-time {
font-weight: bold;
}
.history-toolbar-btn {
.btn;
.btn-info;
.btn-xs;
padding-left: @padding-small-horizontal;
padding-right: @padding-small-horizontal;
margin-left: (@line-height-computed / 2);
}
.history-entries {
font-size: @history-base-font-size;
color: @history-base-color;
height: 100%;
background-color: @history-base-bg;
}
.history-entry-day {
display: block;
background-color: @history-entry-day-bg;
color: #FFF;
padding: 5px 10px;
line-height: 1;
}
.history-entry-details {
background-color: #FFF;
margin-bottom: 2px;
padding: 5px 10px;
cursor: pointer;
.history-entry-selected & {
background-color: @history-entry-selected-bg;
color: #FFF;
}
}
.history-entry-changes {
.list-unstyled;
margin-bottom: 3px;
}
.history-entry-change {
}
.history-entry-change-action {
margin-right: 0.5em;
}
.history-entry-change-doc {
color: @history-highlight-color;
font-weight: bold;
word-break: break-all;
.history-entry-selected & {
color: #FFF;
}
}
.history-entry-metadata {
}
.history-entry-metadata-time {
white-space: nowrap;
}
.history-entry-metadata-users {
display: inline;
padding: 0;
}
.history-entry-metadata-user {
display: inline;
&::after {
content: ', ';
}
&:last-of-type::after {
content: none;
}
}
.history-file-tree-inner {
.full-size;
overflow-y: auto;
background-color: @file-tree-bg;
.loading {
color: #FFF;
font-size: @history-base-font-size;
text-align: center;
font-family: @font-family-serif;
}
}
.history-file-tree-inner when (@is-overleaf = false) {
font-size: 0.8rem;
}
.history-file-entity-wrapper {
color: #FFF;
margin-left: (@line-height-computed / 2);
}
.history-file-entity-link {
display: block;
position: relative;
color: @file-tree-item-color;
line-height: @file-tree-line-height;
&:hover {
background-color: @file-tree-item-hover-bg;
color: @file-tree-item-color;
text-decoration: none;
}
&:focus {
color: @file-tree-item-color;
outline: none;
text-decoration: none;
}
&:hover when (@is-overleaf = true) {
.fake-full-width-bg(@file-tree-item-hover-bg);
}
}
.history-file-entity-link-selected {
background-color: @file-tree-item-selected-bg;
font-weight: bold;
padding-right: 32px;
color: #FFF;
.fake-full-width-bg(@file-tree-item-selected-bg);
&:hover {
background-color: @file-tree-item-hover-bg;
}
}
.history-file-entity-icon {
color: @file-tree-item-icon-color;
font-size: 14px;
margin-right: .5em;
.history-file-entity-link-selected & {
color: #FFF;
}
}
.history-file-entity-name {
display: block;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-file-entity-link-selected when (@is-overleaf = false) {
color: @brand-primary;
&:hover,
&:focus {
color: @brand-primary;
}
.history-file-entity-icon {
color: @brand-primary;
}
}
// @changesListWidth: 250px;
// @changesListPadding: @line-height-computed / 2;
// @selector-padding-vertical: 10px;
// @selector-padding-horizontal: @line-height-computed / 2;
// @day-header-height: 24px;
// @range-bar-color: @link-color;
// @range-bar-selected-offset: 14px;
// #history {
// .upgrade-prompt {
// position: absolute;
// top: 0;
// bottom: 0;
// left: 0;
// right: 0;
// z-index: 100;
// background-color: rgba(128,128,128,0.4);
// .message {
// margin: auto;
// margin-top: 100px;
// padding: (@line-height-computed / 2) @line-height-computed;
// width: 400px;
// background-color: white;
// border-radius: 8px;
// }
// .message-wider {
// width: 650px;
// margin-top: 60px;
// padding: 0;
// }
// .message-header {
// .modal-header;
// }
// .message-body {
// .modal-body;
// }
// }
// .diff-panel {
// .full-size;
// margin-right: @changesListWidth;
// }
// .diff {
// .full-size;
// .toolbar {
// padding: 3px;
// .name {
// float: left;
// padding: 3px @line-height-computed / 4;
// display: inline-block;
// }
// }
// .diff-editor {
// .full-size;
// top: 40px;
// }
// .hide-ace-cursor {
// .ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
// display: none;
// }
// }
// .diff-deleted {
// padding: @line-height-computed;
// }
// .deleted-warning {
// background-color: @brand-danger;
// color: white;
// padding: @line-height-computed / 2;
// margin-right: @line-height-computed / 4;
// }
// &-binary {
// .alert {
// margin: @line-height-computed / 2;
// }
// }
// }
// aside.change-list {
// border-left: 1px solid @editor-border-color;
// height: 100%;
// width: @changesListWidth;
// position: absolute;
// right: 0;
// .loading {
// text-align: center;
// font-family: @font-family-serif;
// }
// ul {
// li.change {
// position: relative;
// user-select: none;
// -ms-user-select: none;
// -moz-user-select: none;
// -webkit-user-select: none;
// .day {
// background-color: #fafafa;
// border-bottom: 1px solid @editor-border-color;
// padding: 4px;
// font-weight: bold;
// text-align: center;
// height: @day-header-height;
// font-size: 14px;
// line-height: 1;
// }
// .selectors {
// input {
// margin: 0;
// }
// position: absolute;
// left: @selector-padding-horizontal;
// top: 0;
// bottom: 0;
// width: 24px;
// .selector-from {
// position: absolute;
// bottom: @selector-padding-vertical;
// left: 0;
// opacity: 0.8;
// }
// .selector-to {
// position: absolute;
// top: @selector-padding-vertical;
// left: 0;
// opacity: 0.8;
// }
// .range {
// position: absolute;
// left: 5px;
// width: 4px;
// top: 0;
// bottom: 0;
// }
// }
// .description {
// padding: (@line-height-computed / 4);
// padding-left: 38px;
// min-height: 38px;
// border-bottom: 1px solid @editor-border-color;
// cursor: pointer;
// &:hover {
// background-color: @gray-lightest;
// }
// }
// .users {
// .user {
// font-size: 0.8rem;
// color: @gray;
// text-transform: capitalize;
// position: relative;
// padding-left: 16px;
// .color-square {
// height: 12px;
// width: 12px;
// border-radius: 3px;
// position: absolute;
// left: 0;
// bottom: 3px;
// }
// .name {
// width: 94%;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// }
// }
// }
// .time {
// float: right;
// color: @gray;
// display: inline-block;
// padding-right: (@line-height-computed / 2);
// font-size: 0.8rem;
// line-height: @line-height-computed;
// }
// .doc {
// font-size: 0.9rem;
// font-weight: bold;
// }
// .action {
// color: @gray;
// text-transform: uppercase;
// font-size: 0.7em;
// margin-bottom: -2px;
// margin-top: 2px;
// &-edited {
// margin-top: 0;
// }
// }
// }
// li.loading-changes, li.empty-message {
// padding: 6px;
// cursor: default;
// &:hover {
// background-color: inherit;
// }
// }
// li.selected {
// border-left: 4px solid @range-bar-color;
// .day {
// padding-left: 0;
// }
// .description {
// padding-left: 34px;
// }
// .selectors {
// left: @selector-padding-horizontal - 4px;
// .range {
// background-color: @range-bar-color;
// }
// }
// }
// li.selected-to {
// .selectors {
// .range {
// top: @range-bar-selected-offset;
// }
// .selector-to {
// opacity: 1;
// }
// }
// }
// li.selected-from {
// .selectors {
// .range {
// bottom: @range-bar-selected-offset;
// }
// .selector-from {
// opacity: 1;
// }
// }
// }
// li.first-in-day {
// .selectors {
// .selector-to {
// top: @day-header-height + @selector-padding-vertical;
// }
// }
// }
// li.first-in-day.selected-to {
// .selectors {
// .range {
// top: @day-header-height + @range-bar-selected-offset;
// }
// }
// }
// }
// ul.hover-state {
// li {
// .selectors {
// .range {
// background-color: transparent;
// top: 0;
// bottom: 0;
// }
// }
// }
// li.hover-selected {
// .selectors {
// .range {
// top: 0;
// background-color: @gray-light;
// }
// }
// }
// li.hover-selected-to {
// .selectors {
// .range {
// top: @range-bar-selected-offset;
// }
// .selector-to {
// opacity: 1;
// }
// }
// }
// li.hover-selected-from {
// .selectors {
// .range {
// bottom: @range-bar-selected-offset;
// }
// .selector-from {
// opacity: 1;
// }
// }
// }
// li.first-in-day.hover-selected-to {
// .selectors {
// .range {
// top: @day-header-height + @range-bar-selected-offset;
// }
// }
// }
// }
// }
// }
// .diff-deleted {
// padding-top: 15px;
// }
// .editor-dark {
// #history {
// aside.change-list {
// border-color: @editor-dark-toolbar-border-color;
// ul li.change {
// .day {
// background-color: darken(@editor-dark-background-color, 10%);
// border-bottom: 1px solid @editor-dark-toolbar-border-color;
// }
// .description {
// border-bottom: 1px solid @editor-dark-toolbar-border-color;
// &:hover {
// background-color: black;
// }
// }
// }
// }
// }
// }

View file

@ -1,4 +1,4 @@
@changesListWidth: 250px;
@changesListWidth: 250px;
@changesListPadding: @line-height-computed / 2;
@selector-padding-vertical: 10px;
@ -40,7 +40,8 @@
}
}
.diff-panel {
.diff-panel,
.point-in-time-panel {
.full-size;
margin-right: @changesListWidth;
}
@ -49,6 +50,7 @@
.full-size;
.toolbar {
padding: 3px;
height: 32px;
.name {
float: left;
padding: 3px @line-height-computed / 4;
@ -57,13 +59,9 @@
}
.diff-editor {
.full-size;
top: 40px;
}
.hide-ace-cursor {
.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
display: none;
}
top: 32px;
}
.diff-deleted {
padding: @line-height-computed;
}
@ -90,6 +88,7 @@
.loading {
text-align: center;
font-family: @font-family-serif;
margin-top: (@line-height-computed / 2);
}
ul {
@ -305,6 +304,12 @@
padding-top: 15px;
}
.hide-ace-cursor {
.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
display: none;
}
}
.editor-dark {
#history {
aside.change-list {

View file

@ -32,7 +32,7 @@
padding-bottom: @line-height-computed * 2;
}
}
.circle {
font-size: 1.5rem;
font-weight: 700;
@ -63,6 +63,12 @@
}
.card .btn { white-space:normal; }
.top-switch {
.currency-dropdown {
margin-right: -15px;
}
}
}
#changePlanSection {
@ -127,4 +133,261 @@ input.paymentTypeOption.ng-valid {
text-align: right;
}
/**
Plans Test
*/
@best-val-height: 35px;
@highlight-border: 3px;
@highlight-color: #d3584b;
@gray-med: #6d6d6d;
@white-med: #fdfdfd;
.more-details {
.best-value {
color: @red;
line-height: @line-height-computed;
}
blockquote {
footer{
/* accessibility fix */
color: @gray-med;
}
}
.btn-header {
font-family: @font-family-sans-serif;
margin-left: 10px;
margin-top: -10px;
text-shadow: 0 0 0;
}
.card-first, .card-last {
background: @white-med;
}
.card-highlighted {
border: @highlight-border solid @gray-lighter;
padding-top: 10px!important;
.best-value {
margin-bottom: 15px;
}
.card-header {
padding-bottom: 22px; /* align hr with other plans */
}
}
.card-header {
margin-bottom: 15px;
}
.circle {
/* accessibility fix */
span.small {
color: rgba(255, 255, 255, 0.85)
}
}
.circle-img {
border-radius: 50%;
float: right;
height: 100px;
overflow: hidden;
position: relative;
width: 100px;
img {
display: inline;
margin: 0 auto;
width: 100%;
}
}
.faq:last-child {
p {
margin-bottom: 0;
}
}
.questions-header {
color: @red;
line-height: 37px;
margin: 0;
text-align: right;
}
.tagline {
margin-bottom: 20px;
}
/* Media Queries */
@media (max-width: @screen-md-min) {
.card-highlighted {
/*override style in cards.less */
margin-top: @line-height-computed!important;
}
.circle-img {
float: left;
margin: 0 15px;
}
}
@media (min-width: @screen-md-min) {
blockquote {
margin-bottom: 0;
}
.faq {
.row:nth-child(2) {
h3 {
margin-top: 0;
}
}
}
}
}
.student-disclaimer {
font-size: 14px; /* match .paymentPageFeatures p */
color: @gray; /* match .paymentPageFeatures p */
margin: 12.5px 0 0 0;
}
/**
Plans Table
*/
.plans-table {
border: 1px solid @gray-lighter;
background-color: @white-med;
margin: @best-val-height 0 15px 0;
table-layout: fixed;
width: 100%;
th, td {
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box; /* needed for firefox when there is bg color */
border: 1px solid @gray-lighter;
padding: 6px;
text-align: center;
vertical-align: middle;
}
td {
font-weight: bold;
}
th {
border-top: 0;
font-family: @headings-font-family;
font-size: @font-size-h2;
font-weight: @headings-font-weight;
line-height: @headings-line-height;
padding: 18px;
}
th:first-child, td:first-child {
border-left: 0;
}
th:last-child, td:last-child {
border-right: 0;
}
td:first-child {
font-weight: bold;
padding-left: 18px;
text-align: left;
}
tr:first-child {
th {
position: relative;
/* keep here position here, otherwise messes up border on safari */
}
}
tr:last-child {
td {
border-bottom: 0;
padding: 18px;
}
/* highlighted column */
td:nth-child(3) {
position: relative;
/* keep here position here, otherwise messes up border on safari when there is a bg color */
&:before {
/* needed for safafi */
border-top: 1px solid @gray-lighter;
content: '';
left: 0;
position: absolute;
top: -1px;
width: 100%;
}
}
td:first-child {
border: 0;
}
}
.fa-check {
color: @green;
}
/* accessibility fixes */
.small {
color: @gray-med;
}
/* highlighted column */
td:nth-child(3), th:nth-child(3) {
background-color: white;
border-left: @highlight-border solid @gray-lighter;
border-right: @highlight-border solid @gray-lighter;
}
.outer {
left: -@highlight-border;
right: -@highlight-border;
position: absolute;
.outer-content {
background: white;
border: @highlight-border solid @gray-lighter;
border-radius: @border-radius-base;
font-size: @font-size-base;
font-family: @font-family-sans-serif;
font-weight: bold;
height: @best-val-height;
padding-top: 10px;
}
}
.outer.outer-top {
top: -@best-val-height;
.outer-content {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom: 0;
}
}
.outer.outer-btm {
bottom: -@best-val-height/2;
.outer-content {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 0;
height: @best-val-height/2;
}
}
/* highlight rows on hover */
tr:hover {
td {
background-color: @gray-lightest;
}
}
tr:first-child:hover {
background-color: transparent;
}
tr:last-child:hover {
background-color: transparent;
td {
background-color: transparent;
}
}
/* tooltip */
sup {
color: @red;
cursor: pointer;
margin-left: 5px;
}
.tooltip.in {
min-width: 200px
}
}

View file

@ -888,6 +888,7 @@
@footer-padding : 2em;
// Editor header
@ide-body-top-offset : 40px;
@toolbar-header-bg-color : transparent;
@toolbar-header-shadow : 0 0 2px #ccc;
@toolbar-btn-color : @link-color;
@ -972,4 +973,15 @@
// System messages
@sys-msg-background : @state-warning-bg;
@sys-msg-color : #333;
@sys-msg-border : 1px solid @common-border-color;
@sys-msg-border : 1px solid @common-border-color;
// v2 History
@history-base-font-size : @font-size-small;
@history-base-bg : @gray-lightest;
@history-entry-day-bg : @gray;
@history-entry-selected-bg : @red;
@history-base-color : @gray-light;
@history-highlight-color : @gray;
@history-toolbar-bg-color : @toolbar-alt-bg-color;
@history-toolbar-color : @text-color;

View file

@ -265,6 +265,17 @@
@log-line-no-color : #FFF;
@log-hints-color : @ol-blue-gray-4;
// v2 History
@history-base-font-size : @font-size-small;
@history-base-bg : @ol-blue-gray-1;
@history-entry-day-bg : @ol-blue-gray-2;
@history-entry-selected-bg : @ol-green;
@history-base-color : @ol-blue-gray-2;
@history-highlight-color : @ol-type-color;
@history-toolbar-bg-color : @editor-toolbar-bg;
@history-toolbar-color : #FFF;
// System messages
@sys-msg-background : @ol-blue;
@sys-msg-color : #FFF;

View file

@ -161,4 +161,7 @@ hr {
margin-top: @line-height-computed / 2;
}
.row-spaced-large {
margin-top: @line-height-computed * 2;
}

View file

@ -122,6 +122,11 @@ cite { font-style: normal; }
text-align: center;
}
// Transformations
.text-capitalize {
text-transform: capitalize;
}
// Contextual backgrounds
// For now we'll leave these alongside the text classes until v4 when we can
// safely shift things around (per SemVer rules).
@ -256,7 +261,14 @@ blockquote {
vertical-align: -0.4em;
line-height: 0.1em;
}
&:after {
content: close-quote;
display: inherit;
height: 0;
visibility: hidden;
}
p {
display: inline;
}

View file

@ -12,6 +12,7 @@ ObjectId = require("mongojs").ObjectId
describe "AuthenticationController", ->
beforeEach ->
tk.freeze(Date.now())
@AuthenticationController = SandboxedModule.require modulePath, requires:
"./AuthenticationManager": @AuthenticationManager = {}
"../User/UserGetter" : @UserGetter = {}
@ -39,7 +40,6 @@ describe "AuthenticationController", ->
@req = new MockRequest()
@res = new MockResponse()
@callback = @next = sinon.stub()
tk.freeze(Date.now())
afterEach ->
tk.reset()

View file

@ -10,6 +10,7 @@ moment = require('moment')
describe 'RestoreManager', ->
beforeEach ->
tk.freeze Date.now() # freeze the time for these tests
@RestoreManager = SandboxedModule.require modulePath, requires:
'../../infrastructure/FileWriter': @FileWriter = {}
'../Uploads/FileSystemImportManager': @FileSystemImportManager = {}
@ -22,7 +23,6 @@ describe 'RestoreManager', ->
@project_id = 'mock-project-id'
@version = 42
@callback = sinon.stub()
tk.freeze Date.now() # freeze the time for these tests
afterEach ->
tk.reset()

View file

@ -116,6 +116,7 @@ describe "RecurlyWrapper", ->
apiKey: 'nonsense'
privateKey: 'private_nonsense'
tk.freeze Date.now() # freeze the time for these tests
@RecurlyWrapper = RecurlyWrapper = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings
"logger-sharelatex":
@ -124,10 +125,11 @@ describe "RecurlyWrapper", ->
log: sinon.stub()
"request": sinon.stub()
describe "sign", ->
after ->
tk.reset()
describe "sign", ->
before (done) ->
tk.freeze Date.now() # freeze the time for these tests
@RecurlyWrapper.sign({
subscription :
plan_code : "gold"
@ -137,9 +139,6 @@ describe "RecurlyWrapper", ->
done()
)
after ->
tk.reset()
it "should be signed correctly", ->
signed = @signature.split("|")[0]
query = @signature.split("|")[1]
@ -1061,4 +1060,4 @@ describe "RecurlyWrapper", ->
@RecurlyWrapper.listAccountActiveSubscriptions @user_id, @callback
it "should return an empty array of subscriptions", ->
@callback.calledWith(null, []).should.equal true
@callback.calledWith(null, []).should.equal true

View file

@ -87,35 +87,69 @@ describe "SubscriptionController", ->
@stubbedCurrencyCode = "GBP"
describe "plansPage", ->
beforeEach (done) ->
beforeEach ->
@req.ip = "1234.3123.3131.333 313.133.445.666 653.5345.5345.534"
@GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode)
@res.callback = done
@SubscriptionController.plansPage(@req, @res)
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
it "should set the recommended currency from the geoiplookup", (done)->
@res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
@GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
done()
describe 'when user is logged in', (done) ->
beforeEach (done) ->
@res.callback = done
@SubscriptionController.plansPage(@req, @res)
it 'should fetch the current user', (done) ->
@UserGetter.getUser.callCount.should.equal 1
done()
it 'should fetch the current user', (done) ->
@UserGetter.getUser.callCount.should.equal 1
done()
it 'should decide not to AB test the plans when signed up before 2016-10-27', (done) ->
@res.renderedVariables.shouldABTestPlans.should.equal false
done()
it 'should decide not to AB test the plans', (done) ->
@res.renderedVariables.shouldABTestPlans.should.equal false
done()
describe 'not dependant on logged in state', (done) ->
# these could have been put in 'when user is not logged in' too
it "should set the recommended currency from the geoiplookup", (done)->
@res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
@GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
done()
it 'should include data for features table', (done) ->
# this is part of AB test. If default wins test, then remove this test
@res.renderedVariables.planFeatures.length.should.not.equal 0
done()
describe 'when user is not logged in', (done) ->
beforeEach (done) ->
@UserGetter =
getUser: sinon.stub().callsArgWith(2, null, null)
@res.callback = done
@SubscriptionController.plansPage(@req, @res)
@AuthenticationController =
getLoggedInUser: sinon.stub().callsArgWith(1, null, null)
getLoggedInUserId: sinon.stub().returns(null)
getSessionUser: sinon.stub().returns(null)
isUserLoggedIn: sinon.stub().returns(false)
beforeEach ->
@AuthenticationController.getLoggedInUserId.returns(null)
@SubscriptionController = SandboxedModule.require modulePath, requires:
'../Authentication/AuthenticationController': @AuthenticationController
'./SubscriptionHandler': @SubscriptionHandler
"./PlansLocator": @PlansLocator
'./SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder
"./LimitationsManager": @LimitationsManager
"../../infrastructure/GeoIpLookup":@GeoIpLookup
"logger-sharelatex":
log:->
warn:->
"settings-sharelatex": @settings
"./SubscriptionDomainHandler":@SubscriptionDomainHandler
"../User/UserGetter": @UserGetter
"./RecurlyWrapper": @RecurlyWrapper = {}
"./FeaturesUpdater": @FeaturesUpdater = {}
it 'should not fetch the current user', (done) ->
@UserGetter.getUser.callCount.should.equal 0
done()
it 'should decide to AB test', (done) ->
@res.renderedVariables.shouldABTestPlans.should.equal true
done()
describe "paymentPage", ->
beforeEach ->
@req.headers = {}
@ -460,4 +494,4 @@ describe "SubscriptionController", ->
@SubscriptionHandler.updateSubscription.calledWith(@user, "collaborator-annual", "COLLABORATORCODEHERE").should.equal true
done()
@SubscriptionController.processUpgradeToAnnualPlan @req, @res
@SubscriptionController.processUpgradeToAnnualPlan @req, @res