diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 32d2abf594..03e87125ac 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -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) ->
diff --git a/services/web/app/coffee/Features/Subscription/planFeatures.coffee b/services/web/app/coffee/Features/Subscription/planFeatures.coffee
new file mode 100644
index 0000000000..8c9c276955
--- /dev/null
+++ b/services/web/app/coffee/Features/Subscription/planFeatures.coffee
@@ -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
+ },
+ ]
\ No newline at end of file
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index b7481725a2..22c4abe925 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -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
diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug
index 7415c2ed77..602b9af86b 100644
--- a/services/web/app/views/project/editor.pug
+++ b/services/web/app/views/project/editor.pug
@@ -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
diff --git a/services/web/app/views/project/editor/history-file-tree.pug b/services/web/app/views/project/editor/history-file-tree.pug
deleted file mode 100644
index 3356dc2249..0000000000
--- a/services/web/app/views/project/editor/history-file-tree.pug
+++ /dev/null
@@ -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 }}
diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index d366d5f41a..a7a52d2927 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -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
diff --git a/services/web/app/views/project/editor/history/diffPanelV1.pug b/services/web/app/views/project/editor/history/diffPanelV1.pug
index 30588adf46..369f3703b0 100644
--- a/services/web/app/views/project/editor/history/diffPanelV1.pug
+++ b/services/web/app/views/project/editor/history/diffPanelV1.pug
@@ -14,7 +14,7 @@
)
| in {{history.diff.pathname}}
.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")}
diff --git a/services/web/app/views/project/editor/history/entriesListV1.pug b/services/web/app/views/project/editor/history/entriesListV1.pug
new file mode 100644
index 0000000000..27f9e66fe1
--- /dev/null
+++ b/services/web/app/views/project/editor/history/entriesListV1.pug
@@ -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 }} → {{ 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
+ | #{translate("loading")}...
diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug
new file mode 100644
index 0000000000..fa7a90b20e
--- /dev/null
+++ b/services/web/app/views/project/editor/history/entriesListV2.pug
@@ -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 }} → {{ 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
+ | #{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
+ | #{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
+ |
+ | •
+ |
+ 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")}
\ No newline at end of file
diff --git a/services/web/app/views/project/editor/history/fileTreeV2.pug b/services/web/app/views/project/editor/history/fileTreeV2.pug
new file mode 100644
index 0000000000..0f3a2c1203
--- /dev/null
+++ b/services/web/app/views/project/editor/history/fileTreeV2.pug
@@ -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"
+ )
diff --git a/services/web/app/views/project/editor/history/diffPanelV2.pug b/services/web/app/views/project/editor/history/previewPanelV2.pug
similarity index 58%
rename from services/web/app/views/project/editor/history/diffPanelV2.pug
rename to services/web/app/views/project/editor/history/previewPanelV2.pug
index 415d587155..3d7a1ac3df 100644
--- a/services/web/app/views/project/editor/history/diffPanelV2.pug
+++ b/services/web/app/views/project/editor/history/previewPanelV2.pug
@@ -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 {{history.diff.pathname}}
+ .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
| #{translate("loading")}...
.error-panel(ng-show="history.diff.error")
- .alert.alert-danger #{translate("generic_something_went_wrong")}
\ No newline at end of file
+ .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
+ | #{translate("loading")}...
+ .error-panel(ng-show="history.selectedFile.error")
+ .alert.alert-danger #{translate("generic_something_went_wrong")}
diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug
new file mode 100644
index 0000000000..799a7136f3
--- /dev/null
+++ b/services/web/app/views/project/editor/history/toolbarV2.pug
@@ -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
+ | #{translate("loading")}...
+ span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}
+ 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")}
\ No newline at end of file
diff --git a/services/web/app/views/subscriptions/_plans_page_details_less.pug b/services/web/app/views/subscriptions/_plans_page_details_less.pug
new file mode 100644
index 0000000000..2aa572e4e7
--- /dev/null
+++ b/services/web/app/views/subscriptions/_plans_page_details_less.pug
@@ -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
+ |
+ i.fa.fa-github.fa-5x
+ h4 #{translate("sync_to_dropbox_and_github")}
+ p #{translate("access_projects_anywhere")}
\ No newline at end of file
diff --git a/services/web/app/views/subscriptions/_plans_page_details_more.pug b/services/web/app/views/subscriptions/_plans_page_details_more.pug
new file mode 100644
index 0000000000..17c683201e
--- /dev/null
+++ b/services/web/app/views/subscriptions/_plans_page_details_more.pug
@@ -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
+ i.fa.fa-cc-visa.fa-2x
+ i.fa.fa-cc-amex.fa-2x
+ i.fa.fa-cc-paypal.fa-2x
+ div.text-centered #{translate('change_plans_any_time')}
#{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: '' + translate('referring_your_friends') + ''})}
+ .row
+ .col-md-6
+ h3 #{translate('faq_purchase_more_licenses_question')}
+ p !{translate('faq_purchase_more_licenses_answer', { groupLink: '' + translate('discounted_group_accounts') + '' })}
+ .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: '' + translate('discounted_group_accounts') + '' })}
+ .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)
diff --git a/services/web/app/views/subscriptions/_plans_page_mixins.pug b/services/web/app/views/subscriptions/_plans_page_mixins.pug
new file mode 100644
index 0000000000..c1f19bca00
--- /dev/null
+++ b/services/web/app/views/subscriptions/_plans_page_mixins.pug
@@ -0,0 +1,162 @@
+//- Buy Buttons
+mixin btn_buy_collaborator(location)
+ a.btn.btn-info(
+ ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}¤cy={{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}}¤cy={{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¤cy={{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}}¤cy={{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")
+ li(class="hidden-xs hidden-sm")
+ li(class="hidden-xs hidden-sm")
+ li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'")
+ li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'")
+ li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'")
+ 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'")
+ 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")}
+
diff --git a/services/web/app/views/subscriptions/_plans_page_tables.pug b/services/web/app/views/subscriptions/_plans_page_tables.pug
new file mode 100644
index 0000000000..63c4747603
--- /dev/null
+++ b/services/web/app/views/subscriptions/_plans_page_tables.pug
@@ -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
+ 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
+ td
+ +btn_buy_student('table', 'monthly')
+
diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug
index d86bdb8166..4eae442f1e 100644
--- a/services/web/app/views/subscriptions/new.pug
+++ b/services/web/app/views/subscriptions/new.pug
@@ -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
diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug
index 7c40c38a52..56a20f90f5 100644
--- a/services/web/app/views/subscriptions/plans.pug
+++ b/services/web/app/views/subscriptions/plans.pug
@@ -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
- li
- li
- 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() }}¤cy={{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}}¤cy={{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
- li
- li
- 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 }}¤cy={{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 }}¤cy={{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
- |
- 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
diff --git a/services/web/public/coffee/directives/eventTracking.coffee b/services/web/public/coffee/directives/eventTracking.coffee
index 9ba2fbb647..2710960fe0 100644
--- a/services/web/public/coffee/directives/eventTracking.coffee
+++ b/services/web/public/coffee/directives/eventTracking.coffee
@@ -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)
}
- ]
\ No newline at end of file
+ ]
diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
index d7a428ec80..8d717e09b8 100644
--- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
+++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
@@ -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
diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee
index 735d065cd8..0dcbac31c1 100644
--- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee
+++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee
@@ -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", [
diff --git a/services/web/public/coffee/ide/file-tree/util/iconTypeFromName.coffee b/services/web/public/coffee/ide/file-tree/util/iconTypeFromName.coffee
new file mode 100644
index 0000000000..01c11f395a
--- /dev/null
+++ b/services/web/public/coffee/ide/file-tree/util/iconTypeFromName.coffee
@@ -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"
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index cdc39ffd05..8c77d97965 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -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
diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
index 72f4c79bdd..7c8f476e39 100644
--- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
@@ -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
diff --git a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee
new file mode 100644
index 0000000000..5022724714
--- /dev/null
+++ b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee
@@ -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"
+ }
diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee
new file mode 100644
index 0000000000..e2692b7dee
--- /dev/null
+++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee
@@ -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"
+ }
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/history/components/historyFileEntity.coffee b/services/web/public/coffee/ide/history/components/historyFileEntity.coffee
new file mode 100644
index 0000000000..bf99348c62
--- /dev/null
+++ b/services/web/public/coffee/ide/history/components/historyFileEntity.coffee
@@ -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"
+ }
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/history/components/historyFileTree.coffee b/services/web/public/coffee/ide/history/components/historyFileTree.coffee
new file mode 100644
index 0000000000..7e3c636470
--- /dev/null
+++ b/services/web/public/coffee/ide/history/components/historyFileTree.coffee
@@ -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"
+ }
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee
index f16cace816..4b0786d259 100644
--- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee
+++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee
@@ -5,7 +5,7 @@ define [
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
$scope.hoveringOverListSelectors = false
-
+
$scope.loadMore = () =>
ide.historyManager.fetchNextBatchOfUpdates()
diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2FileTreeController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2FileTreeController.coffee
new file mode 100644
index 0000000000..47f5c07c4c
--- /dev/null
+++ b/services/web/public/coffee/ide/history/controllers/HistoryV2FileTreeController.coffee
@@ -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
+ ]
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee
new file mode 100644
index 0000000000..eaf7fbc884
--- /dev/null
+++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee
@@ -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()
+ ]
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/history/util/HistoryViewModes.coffee b/services/web/public/coffee/ide/history/util/HistoryViewModes.coffee
new file mode 100644
index 0000000000..125dd87060
--- /dev/null
+++ b/services/web/public/coffee/ide/history/util/HistoryViewModes.coffee
@@ -0,0 +1,4 @@
+define [], () ->
+ HistoryViewModes =
+ POINT_IN_TIME : 'point_in_time'
+ COMPARE : 'compare'
diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee
index 3ff8fc0aa4..d4e07c7fc8 100644
--- a/services/web/public/coffee/main/event.coffee
+++ b/services/web/public/coffee/main/event.coffee
@@ -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)
}
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index 31d8e37f40..7851171524 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -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:'Åland Islandscode:'}
]
+
+ sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)->
+ $scope.plansVariant = chosenVariation
\ No newline at end of file
diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee
index 9a62420d66..7eb63607c0 100644
--- a/services/web/public/coffee/main/plans.coffee
+++ b/services/web/public/coffee/main/plans.coffee
@@ -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
+
diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee
index c2b251bd65..6696243905 100644
--- a/services/web/public/coffee/main/project-list/project-list.coffee
+++ b/services/web/public/coffee/main/project-list/project-list.coffee
@@ -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
}
diff --git a/services/web/public/img/advocates/erdogmus.jpg b/services/web/public/img/advocates/erdogmus.jpg
new file mode 100644
index 0000000000..e70a641fd0
Binary files /dev/null and b/services/web/public/img/advocates/erdogmus.jpg differ
diff --git a/services/web/public/img/advocates/henderson.jpg b/services/web/public/img/advocates/henderson.jpg
new file mode 100644
index 0000000000..549570955c
Binary files /dev/null and b/services/web/public/img/advocates/henderson.jpg differ
diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less
index 52790de9a8..bbe3463cba 100644
--- a/services/web/public/stylesheets/_style_includes.less
+++ b/services/web/public/stylesheets/_style_includes.less
@@ -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
diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less
index fc52bfb156..0e3d48cf87 100644
--- a/services/web/public/stylesheets/app/editor.less
+++ b/services/web/public/stylesheets/app/editor.less
@@ -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;
}
diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less
new file mode 100644
index 0000000000..9089ed1ae9
--- /dev/null
+++ b/services/web/public/stylesheets/app/editor/history-v2.less
@@ -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;
+// }
+// }
+// }
+// }
+// }
+// }
diff --git a/services/web/public/stylesheets/app/editor/history.less b/services/web/public/stylesheets/app/editor/history.less
index 68616f6100..3e558b6c5f 100644
--- a/services/web/public/stylesheets/app/editor/history.less
+++ b/services/web/public/stylesheets/app/editor/history.less
@@ -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 {
diff --git a/services/web/public/stylesheets/app/plans.less b/services/web/public/stylesheets/app/plans.less
index b830db9b23..41e2fc80f1 100644
--- a/services/web/public/stylesheets/app/plans.less
+++ b/services/web/public/stylesheets/app/plans.less
@@ -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
+ }
+}
diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less
index 9cb074c754..033ce9c1bf 100644
--- a/services/web/public/stylesheets/core/_common-variables.less
+++ b/services/web/public/stylesheets/core/_common-variables.less
@@ -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;
\ No newline at end of file
+@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;
+
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index e9125e0143..3d3d4468fd 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -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;
diff --git a/services/web/public/stylesheets/core/scaffolding.less b/services/web/public/stylesheets/core/scaffolding.less
index df52ab1fb4..abadcabae9 100755
--- a/services/web/public/stylesheets/core/scaffolding.less
+++ b/services/web/public/stylesheets/core/scaffolding.less
@@ -161,4 +161,7 @@ hr {
margin-top: @line-height-computed / 2;
}
+.row-spaced-large {
+ margin-top: @line-height-computed * 2;
+}
diff --git a/services/web/public/stylesheets/core/type.less b/services/web/public/stylesheets/core/type.less
index ac1dcf3765..c8f2dcb9f1 100755
--- a/services/web/public/stylesheets/core/type.less
+++ b/services/web/public/stylesheets/core/type.less
@@ -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;
}
diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
index 92d2a7dbdb..1d8d8ab27d 100644
--- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -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()
diff --git a/services/web/test/unit/coffee/History/RestoreManagerTests.coffee b/services/web/test/unit/coffee/History/RestoreManagerTests.coffee
index 37fe6752e7..012c720a2b 100644
--- a/services/web/test/unit/coffee/History/RestoreManagerTests.coffee
+++ b/services/web/test/unit/coffee/History/RestoreManagerTests.coffee
@@ -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()
diff --git a/services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee b/services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee
index d951653310..c4f6aff4c1 100644
--- a/services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee
+++ b/services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee
@@ -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
\ No newline at end of file
+ @callback.calledWith(null, []).should.equal true
diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
index 04888e2dd7..1592215040 100644
--- a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
+++ b/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
@@ -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
\ No newline at end of file