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