diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js
index abddcbe0c1..f848e7bd72 100644
--- a/services/web/app/src/Features/Project/ProjectController.js
+++ b/services/web/app/src/Features/Project/ProjectController.js
@@ -412,7 +412,7 @@ const ProjectController = {
user(cb) {
User.findById(
userId,
- 'featureSwitches overleaf awareOfV2 features lastLoginIp',
+ 'emails featureSwitches overleaf awareOfV2 features lastLoginIp',
cb
)
},
@@ -428,27 +428,92 @@ const ProjectController = {
logger.warn({ err }, 'error getting data for project list page')
return next(err)
}
+ const { notifications, user, userAffiliations } = results
const v1Tags =
(results.v1Projects != null ? results.v1Projects.tags : undefined) ||
[]
const tags = results.tags.concat(v1Tags)
- const notifications = results.notifications
for (const notification of notifications) {
notification.html = req.i18n.translate(
notification.templateKey,
notification.messageOpts
)
}
+
+ // Institution SSO Notifications
+ if (Features.hasFeature('saml') || req.session.samlBeta) {
+ const samlSession = req.session.saml
+ // Notification: SSO Available
+ // Could have multiple emails at the same institution, and if any are
+ // linked to the institution then do not show notification for others
+ const linkedInstitutionIds = []
+ const linkedInstitutionEmails = []
+ user.emails.forEach(email => {
+ if (email.samlProviderId) {
+ linkedInstitutionEmails.push(email.email)
+ linkedInstitutionIds.push(email.samlProviderId)
+ }
+ })
+ if (Array.isArray(userAffiliations)) {
+ userAffiliations.forEach(affiliation => {
+ if (
+ affiliation.institution &&
+ affiliation.institution.ssoEnabled &&
+ linkedInstitutionEmails.indexOf(affiliation.email) === -1 &&
+ linkedInstitutionIds.indexOf(
+ affiliation.institution.id.toString()
+ ) === -1
+ ) {
+ notifications.push({
+ email: affiliation.email,
+ institutionId: affiliation.institution.id,
+ institutionName: affiliation.institution.name,
+ templateKey: 'notification_institution_sso_available'
+ })
+ }
+ })
+ }
+
+ if (samlSession) {
+ // Notification: After SSO Linked
+ if (samlSession.linked) {
+ notifications.push({
+ email: samlSession.institutionEmail,
+ institutionName: samlSession.linked.universityName,
+ templateKey: 'notification_institution_sso_linked'
+ })
+ }
+
+ // Notification: After SSO Linked or Logging in
+ // The requested email does not match primary email returned from
+ // the institution
+ if (samlSession.emailNonCanonical) {
+ notifications.push({
+ institutionEmail: samlSession.emailNonCanonical,
+ requestedEmail: samlSession.requestedEmail,
+ templateKey: 'notification_institution_sso_non_canonical'
+ })
+ }
+
+ // Notification: Tried to register, but account already existed
+ if (samlSession.registerIntercept) {
+ notifications.push({
+ email: samlSession.institutionEmail,
+ templateKey: 'notification_institution_sso_already_registered'
+ })
+ }
+ }
+ delete req.session.saml
+ }
+
const portalTemplates = ProjectController._buildPortalTemplatesList(
- results.userAffiliations
+ userAffiliations
)
const projects = ProjectController._buildProjectList(
results.projects,
userId,
results.v1Projects != null ? results.v1Projects.projects : undefined
)
- const { user } = results
- const { userAffiliations } = results
const warnings = ProjectController._buildWarningsList(
results.v1Projects
)
diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js
index 95c13e3a48..3be4be4e77 100644
--- a/services/web/app/src/Features/User/UserPagesController.js
+++ b/services/web/app/src/Features/User/UserPagesController.js
@@ -115,7 +115,16 @@ const UserPagesController = {
delete req.session.ssoError
}
// Institution SSO
- const institutionLinked = _.get(req.session, ['saml', 'linked'])
+ let institutionLinked = _.get(req.session, ['saml', 'linked'])
+ if (institutionLinked) {
+ // copy object if exists because _.get does not
+ institutionLinked = Object.assign(
+ {
+ hasEntitlement: _.get(req.session, ['saml', 'hasEntitlement'])
+ },
+ institutionLinked
+ )
+ }
const institutionNotLinked = _.get(req.session, ['saml', 'notLinked'])
const institutionEmailNonCanonical = _.get(req.session, [
'saml',
diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug
index 9e4f22c51d..555f534c5c 100644
--- a/services/web/app/views/project/list.pug
+++ b/services/web/app/views/project/list.pug
@@ -89,8 +89,11 @@ block content
include ./list/project-list
.project-list-empty.row(ng-if="projects.length === 0")
- .col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
+ .project-list-empty-col.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8.col-xs-offset-2
include ./list/empty-project-list
+ .row.row-spaced
+ .col-sm-12
+ include ./list/notifications
include ./list/modals
diff --git a/services/web/app/views/project/list/notifications.pug b/services/web/app/views/project/list/notifications.pug
index 2fd7db6920..cb3d9abbf6 100644
--- a/services/web/app/views/project/list/notifications.pug
+++ b/services/web/app/views/project/list/notifications.pug
@@ -1,77 +1,106 @@
-span(ng-controller="NotificationsController").userNotifications
- ul.list-unstyled.notifications-list(
+.user-notifications(ng-controller="NotificationsController")
+ ul.list-unstyled(
ng-if="notifications.length > 0",
ng-cloak
)
- li.notification_entry(
+ li.notification-entry(
ng-repeat="notification in notifications",
)
- .row(ng-hide="notification.hide")
- .col-xs-12
- div(ng-switch="notification.templateKey")
- .alert.alert-info(ng-switch-when="notification_project_invite", ng-controller="ProjectInviteNotificationController")
- div.notification_inner
- .notification_body(ng-show="!notification.accepted")
- | !{translate("notification_project_invite_message", { userName: "{{ userName }}", projectName: "{{ projectName }}" })}
- a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight")
- span(ng-show="!notification.inflight") #{translate("join_project")}
- span(ng-show="notification.inflight")
- i.fa.fa-fw.fa-spinner.fa-spin(aria-hidden="true")
- |
- | #{translate("joining")}...
- .notification_body(ng-show="notification.accepted")
- | !{translate("notification_project_invite_accepted_message", { projectName: "{{ projectName }}" })}
- a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")}
- span().notification_close
- button(ng-click="dismiss(notification)").close.pull-right
- span(aria-hidden="true") ×
- span.sr-only #{translate("close")}
- .alert.alert-info(ng-switch-when="notification_ip_matched_affiliation")
- div.notification_inner
- .notification_body
- | It looks like you're at
- strong {{ notification.messageOpts.university_name }}!
- | Did you know that {{notification.messageOpts.university_name}} is providing
- strong free Overleaf Professional accounts
- | to everyone at {{notification.messageOpts.university_name}}?
- | Add an institutional email address to claim your account.
- a.pull-right.btn.btn-sm.btn-info(href="/user/settings")
- | Add Affiliation
- span().notification_close
- button(ng-click="dismiss(notification)").close.pull-right
- span(aria-hidden="true") ×
- span.sr-only #{translate("close")}
- .alert.alert-info(ng-switch-default)
- div.notification_inner
- span(ng-bind-html="notification.html").notification_body
- span().notification_close
- button(ng-click="dismiss(notification)").close.pull-right
- span(aria-hidden="true") ×
- span.sr-only #{translate("close")}
+ div(ng-switch="notification.templateKey" ng-hide="notification.hide")
+ .alert.alert-info(ng-switch-when="notification_project_invite", ng-controller="ProjectInviteNotificationController")
+ .notification-body(ng-show="!notification.accepted")
+ | !{translate("notification_project_invite_message", { userName: "{{ userName }}", projectName: "{{ projectName }}" })}
+ a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight")
+ span(ng-show="!notification.inflight") #{translate("join_project")}
+ span(ng-show="notification.inflight")
+ i.fa.fa-fw.fa-spinner.fa-spin(aria-hidden="true")
+ |
+ | #{translate("joining")}...
+ .notification-body(ng-show="notification.accepted")
+ | !{translate("notification_project_invite_accepted_message", { projectName: "{{ projectName }}" })}
+ a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")}
+ .notification-close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
+ .alert.alert-info(ng-switch-when="notification_ip_matched_affiliation")
+ .notification-body
+ | It looks like you're at
+ strong {{ notification.messageOpts.university_name }}!
+ | Did you know that {{notification.messageOpts.university_name}} is providing
+ strong free Overleaf Professional accounts
+ | to everyone at {{notification.messageOpts.university_name}}?
+ | Add an institutional email address to claim your account.
+ a.pull-right.btn.btn-sm.btn-info(href="/user/settings")
+ | Add Affiliation
+ .notification-close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
+ .alert.alert-info(ng-switch-when="notification_institution_sso_available")
+ .notification-body
+ p !{translate("can_link_institution_email_acct_to_institution_acct", {appName: settings.appName, email: "{{notification.email}}", institutionName: "{{notification.institutionName}}"})}
+ div !{translate("doing_this_allow_log_in_through_institution", {appName: settings.appName})}
+ .notification-action
+ a.btn.btn-info(href="{{samlInitPath}}?university_id={{notification.institutionId}}&auto=project&email={{notification.email}}")
+ | #{translate('link_account')}
+ .notification-close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
+ .alert.alert-info(ng-switch-when="notification_institution_sso_linked")
+ .notification-body
+ div !{translate("account_has_been_link_to_institution_account", {appName: settings.appName, email: "{{notification.email}}", institutionName: "{{notification.institutionName}}"})}
+ .notification-close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
+ .alert.alert-warning(ng-switch-when="notification_institution_sso_non_canonical")
+ .notification-body
+ div
+ i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true")
+ | !{translate("tried_to_log_in_with_email", {email: "{{notification.requestedEmail}}"})} !{translate("in_order_to_match_institutional_metadata_associated", {email: "{{notification.institutionEmail}}"})}
+ .notification-close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
+ .alert.alert-info(ng-switch-when="notification_institution_sso_already_registered")
+ .notification-body
+ | !{translate("tried_to_register_with_email", {appName: settings.appName, email: "{{notification.email}}"})}
+ | #{translate("we_logged_you_in")}
+ .notification-action
+ a.btn.btn-info(href="/")
+ | #{translate("find_out_more")}
+ .notification-close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
+ .alert.alert-info(ng-switch-default)
+ span(ng-bind-html="notification.html").notification-body
+ .notification-close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
- ul.list-unstyled.notifications-list(
+
+ ul.list-unstyled(
ng-controller="EmailNotificationController",
ng-cloak
)
- li.notification_entry(
+ li.notification-entry(
ng-repeat="userEmail in userEmails",
ng-if="showConfirmEmail(userEmail)"
)
- .row
- .col-xs-12
- .alert.alert-warning
- .notification_inner(
- ng-if="!userEmail.confirmationInflight"
- )
- | #{translate("please_confirm_email", {emailAddress: "{{ userEmail.email }}"})}
- |
- a(
- href
- ng-click="resendConfirmationEmail(userEmail)"
- ) (#{translate('resend_confirmation_email')})
- .notification_inner(
- ng-if="userEmail.confirmationInflight"
- )
- i.fa.fa-spinner.fa-spin(aria-hidden="true")
- |
- | #{translate('resending_confirmation_email')}…
+ .alert.alert-warning(ng-if="!userEmail.confirmationInflight")
+ .notification-body
+ | #{translate("please_confirm_email", {emailAddress: "{{ userEmail.email }}"})}
+ |
+ a(
+ href
+ ng-click="resendConfirmationEmail(userEmail)"
+ ) (#{translate('resend_confirmation_email')})
+ .alert.alert-warning(ng-if="userEmail.confirmationInflight")
+ .notification-body
+ i.fa.fa-spinner.fa-spin(aria-hidden="true")
+ |
+ | #{translate('resending_confirmation_email')}…
\ No newline at end of file
diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug
index b0b56e190f..8860ecc4e6 100644
--- a/services/web/app/views/user/settings/user-affiliations.pug
+++ b/services/web/app/views/user/settings/user-affiliations.pug
@@ -212,7 +212,7 @@ form.row(
span(ng-if="ui.errorMessage") {{ui.errorMessage}}
if institutionLinked
tr.affiliations-table-info-row(ng-if="!hideInstitutionNotifications.info")
- td(colspan="3").text-center(aria-live="assertive")
+ td.text-center(aria-live="assertive" colspan="3")
button.close(
type="button"
data-dismiss="modal"
@@ -225,7 +225,7 @@ form.row(
.small !{translate("this_grants_access_to_features", {featureType: translate("professional")})}
if institutionEmailNonCanonical
tr.affiliations-table-warning-row(ng-if="!hideInstitutionNotifications.warning")
- td(colspan="3").text-center(aria-live="assertive")
+ td.text-center(aria-live="assertive" colspan="3")
button.close(
type="button"
data-dismiss="modal"
@@ -239,7 +239,7 @@ form.row(
| !{translate("in_order_to_match_institutional_metadata", {email: institutionEmailNonCanonical})}
if institutionNotLinked
tr.affiliations-table-error-row(ng-if="!hideInstitutionNotifications.error")
- td(colspan="3").text-center(aria-live="assertive")
+ td.text-center(aria-live="assertive" colspan="3")
button.close(
type="button"
data-dismiss="modal"
diff --git a/services/web/public/src/main/project-list/notifications-controller.js b/services/web/public/src/main/project-list/notifications-controller.js
index 0115de610c..4beb6fd6b5 100644
--- a/services/web/public/src/main/project-list/notifications-controller.js
+++ b/services/web/public/src/main/project-list/notifications-controller.js
@@ -17,6 +17,8 @@ define(['base'], function(App) {
notification.hide = false
}
+ $scope.samlInitPath = ExposedSettings.samlInitPath
+
return ($scope.dismiss = notification =>
$http({
url: `/notifications/${notification._id}`,
diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less
index 56b945713b..bfd804b3a5 100644
--- a/services/web/public/stylesheets/app/project-list.less
+++ b/services/web/public/stylesheets/app/project-list.less
@@ -1,730 +1,769 @@
-@import "./list/v1-import-modal.less";
+@import './list/v1-import-modal.less';
@announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
@keyframes pulse {
- 0% {
- opacity: .7;
- }
- 100% {
- opacity: .9;
- }
+ 0% {
+ opacity: 0.7;
+ }
+ 100% {
+ opacity: 0.9;
+ }
}
@keyframes fade-in {
- 0% {
- opacity: 0;
- }
- 100% {
- opacity: 1;
- }
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
}
.project-list-page {
- position: absolute;
- top: @header-height;
- bottom: @footer-height;
- padding-bottom: 0;
- width: 100%;
- overflow-x: hidden;
- overflow-y: auto;
- // Specificity needed to override default `body > .content` values
- body > .content& {
- min-height: calc(~"100vh -" (@header-height + @footer-height));
- padding-top: 0;
- }
+ position: absolute;
+ top: @header-height;
+ bottom: @footer-height;
+ padding-bottom: 0;
+ width: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+ // Specificity needed to override default `body > .content` values
+ body > .content& {
+ min-height: calc(~'100vh -' (@header-height + @footer-height));
+ padding-top: 0;
+ }
}
.project-list-content when (@is-overleaf) {
- margin: 0;
- height: 100%;
- overflow: hidden;
+ margin: 0;
+ height: 100%;
+ overflow: hidden;
}
.project-list-content when (@is-overleaf = false) {
- .container;
+ .container;
+}
+
+.project-list-empty {
+ height: 100%;
+ overflow-y: scroll;
+}
+.project-list-empty-col {
+ display: flex;
+ height: 100%;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ .row:first-child {
+ flex-grow: 1; /* fill vertical space so notifications are pushed to bottom */
+ }
}
.sidebar-new-proj-btn when (@is-overleaf) {
- .btn-block;
+ .btn-block;
}
- .project-list-row when (@is-overleaf) {
- height: 100%;
- }
- .project-list-container when (@is-overleaf) {
- height: 100%;
- }
- .project-list-sidebar {
- background-color: @sidebar-bg;
- padding-top: @content-margin-vertical;
- padding-bottom: @content-margin-vertical;
- color: @sidebar-color;
- .small {
- color: @sidebar-color;
- }
- }
+.project-list-row when (@is-overleaf) {
+ height: 100%;
+ min-height: calc(~'100vh -' (@header-height + @footer-height));
+}
+.project-list-container when (@is-overleaf) {
+ height: 100%;
+}
+.project-list-sidebar {
+ background-color: @sidebar-bg;
+ padding-top: @content-margin-vertical;
+ padding-bottom: @content-margin-vertical;
+ color: @sidebar-color;
+ .small {
+ color: @sidebar-color;
+ }
+}
- .project-list-sidebar when (@is-overleaf) {
- overflow-x: hidden;
- overflow-y: auto;
- -ms-overflow-style: -ms-autohiding-scrollbar;
- }
+.project-list-sidebar when (@is-overleaf) {
+ overflow-x: hidden;
+ overflow-y: auto;
+ -ms-overflow-style: -ms-autohiding-scrollbar;
+}
- .project-list-main {
- padding-top: @content-margin-vertical;
- padding-bottom: @content-margin-vertical;
- height: 100%;
- margin-left: -(@grid-gutter-width / 2);
- }
+.project-list-main {
+ padding-top: @content-margin-vertical;
+ padding-bottom: @content-margin-vertical;
+ height: 100%;
+ margin-left: -(@grid-gutter-width / 2);
+ overflow-y: scroll;
+}
.project-header {
- .btn-group > .btn {
- padding-left: @line-height-base / 2;
- padding-right: @line-height-base / 2;
- }
+ .btn-group > .btn {
+ padding-left: @line-height-base / 2;
+ padding-right: @line-height-base / 2;
+ }
}
.project-search {
- margin: @line-height-base 0;
+ margin: @line-height-base 0;
}
.project-tools {
- display: inline;
- float: right;
+ display: inline;
+ float: right;
}
.tags-dropdown-menu {
- max-width: 50vw;
+ max-width: 50vw;
- &.dropdown-menu > li > a {
- overflow: hidden;
- text-overflow: ellipsis;
- }
+ &.dropdown-menu > li > a {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
}
.project-list-table {
- width: 100%;
- table-layout: fixed;
+ width: 100%;
+ table-layout: fixed;
+}
+.project-list-table-header-row {
+ border-bottom: 1px solid @structured-list-border-color;
}
- .project-list-table-header-row {
- border-bottom: 1px solid @structured-list-border-color;
- }
- .project-list-table-row {
- position: relative;
- border-bottom: 1px solid @structured-list-border-color;
-
- &:last-child {
- border-bottom: 0 none;
- }
- &:hover {
- background-color: @structured-list-hover-color;
- }
- &:first-child {
- border-bottom-color: @structured-header-border-color;
- &:hover {
- background-color: transparent;
- }
- }
- }
- .project-list-table-name-cell,
- .project-list-table-owner-cell,
- .project-list-table-lastupdated-cell,
- .project-list-table-actions-cell,
- .project-list-table-no-projects-cell {
- padding: (@line-height-computed / 4) 0;
- vertical-align: top;
- }
-
- .project-list-table-no-projects-cell {
- text-align: center;
- }
+.project-list-table-row {
+ position: relative;
+ border-bottom: 1px solid @structured-list-border-color;
- .project-list-table-name-cell {
- width: 50%;
- padding-right: @line-height-computed / 2;
+ &:last-child {
+ border-bottom: 0 none;
+ }
+ &:hover {
+ background-color: @structured-list-hover-color;
+ }
+ &:first-child {
+ border-bottom-color: @structured-header-border-color;
+ &:hover {
+ background-color: transparent;
+ }
+ }
+}
+.project-list-table-name-cell,
+.project-list-table-owner-cell,
+.project-list-table-lastupdated-cell,
+.project-list-table-actions-cell,
+.project-list-table-no-projects-cell {
+ padding: (@line-height-computed / 4) 0;
+ vertical-align: top;
+}
- @media (min-width: @screen-md) {
- width: 47%;
- }
+.project-list-table-no-projects-cell {
+ text-align: center;
+}
- @media (min-width: @screen-lg) {
- width: 50%;
- }
- }
+.project-list-table-name-cell {
+ width: 50%;
+ padding-right: @line-height-computed / 2;
- .project-list-table-name-container {
- position: relative;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- // Extra specificity needed to override Bootstrap's own specificity.
- input.project-list-table-select-item[type="checkbox"] {
- position: absolute;
- left: @line-height-computed - (@grid-gutter-width / 2);
- margin-top: 5px;
- }
+ @media (min-width: @screen-md) {
+ width: 47%;
+ }
- .project-list-table-v1-badge-container {
- position: absolute;
- }
+ @media (min-width: @screen-lg) {
+ width: 50%;
+ }
+}
- .project-list-table-name {
- display: inline-block;
- padding-left: @line-height-computed * 1.5;
- vertical-align: top;
- }
+.project-list-table-name-container {
+ position: relative;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+// Extra specificity needed to override Bootstrap's own specificity.
+input.project-list-table-select-item[type='checkbox'] {
+ position: absolute;
+ left: @line-height-computed - (@grid-gutter-width / 2);
+ margin-top: 5px;
+}
- .project-list-table-name-link {
- padding: 0;
- }
+.project-list-table-v1-badge-container {
+ position: absolute;
+}
- .project-list-table-owner-cell {
- width: 23%;
- padding-right: @line-height-computed / 2;
- overflow: hidden;
- text-overflow: ellipsis;
+.project-list-table-name {
+ display: inline-block;
+ padding-left: @line-height-computed * 1.5;
+ vertical-align: top;
+}
- @media (min-width: @screen-sm) {
- width: 16%;
- }
+.project-list-table-name-link {
+ padding: 0;
+}
- @media (min-width: @screen-md) {
- width: 18%;
- }
+.project-list-table-owner-cell {
+ width: 23%;
+ padding-right: @line-height-computed / 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
- @media (min-width: @screen-lg) {
- width: 16%;
- }
- }
+ @media (min-width: @screen-sm) {
+ width: 16%;
+ }
- .project-list-table-lastupdated-cell {
- width: 27%;
- padding-right: @line-height-computed / 2;
- overflow: hidden;
- text-overflow: ellipsis;
+ @media (min-width: @screen-md) {
+ width: 18%;
+ }
- @media (min-width: @screen-sm) {
- width: 19%;
- }
+ @media (min-width: @screen-lg) {
+ width: 16%;
+ }
+}
- @media (min-width: @screen-md) {
- width: 24%;
- }
- }
+.project-list-table-lastupdated-cell {
+ width: 27%;
+ padding-right: @line-height-computed / 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
- .project-list-table-actions-cell {
- display: none;
- padding-right: @line-height-computed - (@grid-gutter-width / 2);
- text-align: right;
- white-space: nowrap;
+ @media (min-width: @screen-sm) {
+ width: 19%;
+ }
- @media (min-width: @screen-sm) {
- display: table-cell;
- width: 18%;
- }
+ @media (min-width: @screen-md) {
+ width: 24%;
+ }
+}
- @media (min-width: @screen-md) {
- width: 11%;
- }
-
- @media (min-width: @screen-lg) {
- width: 10%;
- }
- }
- .action-btn {
- padding: 0 0.3em;
- margin-left: 0.2em;
- }
+.project-list-table-actions-cell {
+ display: none;
+ padding-right: @line-height-computed - (@grid-gutter-width / 2);
+ text-align: right;
+ white-space: nowrap;
+
+ @media (min-width: @screen-sm) {
+ display: table-cell;
+ width: 18%;
+ }
+
+ @media (min-width: @screen-md) {
+ width: 11%;
+ }
+
+ @media (min-width: @screen-lg) {
+ width: 10%;
+ }
+}
+.action-btn {
+ padding: 0 0.3em;
+ margin-left: 0.2em;
+}
.first-project {
- width: 127px;
- text-align: center;
+ width: 127px;
+ text-align: center;
}
.user-profile {
- .progress {
- height: @line-height-computed / 2;
- margin-bottom: @line-height-computed / 4;
- }
- p {
- margin-bottom: @line-height-computed / 4;
- }
+ .progress {
+ height: @line-height-computed / 2;
+ margin-bottom: @line-height-computed / 4;
+ }
+ p {
+ margin-bottom: @line-height-computed / 4;
+ }
}
-.userNotifications {
- ul {
- margin-bottom:0px;
- }
- .notification_entry {
- .alert {
- .box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25));
- .notification_inner {
- display: table-row;
- .notification_body {
- display: table-cell;
- width: 99%;
- padding-right: 15px;
- vertical-align: middle;
- }
- }
- }
- }
+.user-notifications {
+ ul {
+ margin-bottom: 0px;
+ }
+ .notification-entry {
+ .alert {
+ .box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25));
+ display: flex;
+ flex-wrap: wrap;
+ @media (min-width: @screen-sm-min) {
+ flex-wrap: nowrap;
+ }
+ }
+ }
+}
+.notification-body {
+ flex-grow: 1;
+ width: 90%;
+ @media (min-width: @screen-sm-min) {
+ width: auto;
+ }
+}
+
+.notification-action {
+ margin-top: (@line-height-computed / 2); // match paragraph padding
+ order: 1;
+ @media (min-width: @screen-sm-min) {
+ margin-top: 0;
+ order: 0;
+ padding-left: @padding-sm;
+ }
+}
+
+.notification-close {
+ padding-left: @padding-sm;
+ text-align: right;
+ width: 10%;
+ @media (min-width: @screen-sm-min) {
+ width: auto;
+ }
}
ul.folders-menu {
- margin: @folders-menu-margin;
- .subdued {
- color: @gray-light;
- }
- > li {
- cursor: pointer;
- position: relative;
- > a {
- display: block;
- color: @sidebar-link-color;
- padding: @folders-menu-item-v-padding @folders-menu-item-h-padding;
- border-bottom: solid 1px transparent;
- &:hover {
- background-color: @sidebar-hover-bg;
- text-decoration: @sidebar-hover-text-decoration;
- }
- &:focus {
- text-decoration: none;
- }
- }
- > a when (@is-overleaf = false) {
- font-size: 0.9rem;
- }
- &.separator {
- padding: @folders-menu-item-v-padding @folders-menu-item-h-padding;
- cursor: auto;
- }
- }
- > li.active {
- border-radius: @sidebar-active-border-radius;
- > a {
- background-color: @sidebar-active-bg;
- font-weight: @sidebar-active-font-weight;
- color: @sidebar-active-color;
- .subdued {
- color: @sidebar-active-color;
- }
- }
- }
- > li > a.small {
- color: @gray;
- }
- h2 {
- margin-top: @folders-title-margin-top;
- margin-bottom: @folders-title-margin-bottom;
- font-size: @folders-title-font-size;
- color: @folders-title-color;
- text-transform: @folders-title-text-transform;
- padding: @folders-title-padding;
- font-weight: @folders-title-font-weight;
- font-family: @font-family-sans-serif;
- }
- > li.tag {
- &.active {
- .tag-menu > a {
- color: white;
- border-color: white;
- &:hover {
- background-color: @folders-tag-menu-active-hover;
- }
- }
- }
- &.untagged {
- a.tag-name {
- span.name {
- font-style: italic;
- padding-left: 0;
- }
- }
- }
- &:hover {
- &:not(.active) {
- background-color: @folders-tag-hover;
- }
- .tag-menu {
- display: block
- }
- }
- &:not(.active) {
- .tag-menu > a:hover {
- background-color: @folders-tag-menu-hover;
- }
- }
- a.tag-name {
- position: relative;
- padding: @folders-tag-padding;
- display: @folders-tag-display;
- span.name {
- padding-left: 0.5em;
- line-height: @folders-tag-line-height;
- }
- }
- .tag-menu {
- > a {
- border: 1px solid @folders-tag-border-color;
- border-radius: @border-radius-small;
- color: @folders-tag-menu-color;
- display: block;
- width: 16px;
- height: 16px;
- position: relative;
- .caret {
- position: absolute;
- top: 6px;
- left: 1px;
- }
- }
- display: none;
- position: absolute;
- top: 50%;
- margin-top: -8px; // Half the element height.
- right: 4px;
- &.open {
- display: block;
- }
- }
- }
+ margin: @folders-menu-margin;
+ .subdued {
+ color: @gray-light;
+ }
+ > li {
+ cursor: pointer;
+ position: relative;
+ > a {
+ display: block;
+ color: @sidebar-link-color;
+ padding: @folders-menu-item-v-padding @folders-menu-item-h-padding;
+ border-bottom: solid 1px transparent;
+ &:hover {
+ background-color: @sidebar-hover-bg;
+ text-decoration: @sidebar-hover-text-decoration;
+ }
+ &:focus {
+ text-decoration: none;
+ }
+ }
+ > a when (@is-overleaf = false) {
+ font-size: 0.9rem;
+ }
+ &.separator {
+ padding: @folders-menu-item-v-padding @folders-menu-item-h-padding;
+ cursor: auto;
+ }
+ }
+ > li.active {
+ border-radius: @sidebar-active-border-radius;
+ > a {
+ background-color: @sidebar-active-bg;
+ font-weight: @sidebar-active-font-weight;
+ color: @sidebar-active-color;
+ .subdued {
+ color: @sidebar-active-color;
+ }
+ }
+ }
+ > li > a.small {
+ color: @gray;
+ }
+ h2 {
+ margin-top: @folders-title-margin-top;
+ margin-bottom: @folders-title-margin-bottom;
+ font-size: @folders-title-font-size;
+ color: @folders-title-color;
+ text-transform: @folders-title-text-transform;
+ padding: @folders-title-padding;
+ font-weight: @folders-title-font-weight;
+ font-family: @font-family-sans-serif;
+ }
+ > li.tag {
+ &.active {
+ .tag-menu > a {
+ color: white;
+ border-color: white;
+ &:hover {
+ background-color: @folders-tag-menu-active-hover;
+ }
+ }
+ }
+ &.untagged {
+ a.tag-name {
+ span.name {
+ font-style: italic;
+ padding-left: 0;
+ }
+ }
+ }
+ &:hover {
+ &:not(.active) {
+ background-color: @folders-tag-hover;
+ }
+ .tag-menu {
+ display: block;
+ }
+ }
+ &:not(.active) {
+ .tag-menu > a:hover {
+ background-color: @folders-tag-menu-hover;
+ }
+ }
+ a.tag-name {
+ position: relative;
+ padding: @folders-tag-padding;
+ display: @folders-tag-display;
+ span.name {
+ padding-left: 0.5em;
+ line-height: @folders-tag-line-height;
+ }
+ }
+ .tag-menu {
+ > a {
+ border: 1px solid @folders-tag-border-color;
+ border-radius: @border-radius-small;
+ color: @folders-tag-menu-color;
+ display: block;
+ width: 16px;
+ height: 16px;
+ position: relative;
+ .caret {
+ position: absolute;
+ top: 6px;
+ left: 1px;
+ }
+ }
+ display: none;
+ position: absolute;
+ top: 50%;
+ margin-top: -8px; // Half the element height.
+ right: 4px;
+ &.open {
+ display: block;
+ }
+ }
+ }
}
form.project-search {
- .form-group {
- margin-bottom: 0;
- }
+ .form-group {
+ margin-bottom: 0;
+ }
}
ul.structured-list {
- list-style-type: none;
- margin: 0;
- overflow: hidden;
- overflow-y: auto;
- -ms-overflow-style: -ms-autohiding-scrollbar;
- li {
- border-bottom: 1px solid @structured-list-border-color;
- padding: (@line-height-computed / 4) 0;
-
- &:last-child {
- border-bottom: 0 none;
- }
- &:hover {
- background-color: @structured-list-hover-color;
- }
- &:first-child {
- border-bottom-color: @structured-header-border-color;
- &:hover {
- background-color: transparent;
- }
- }
- a {
- color: @structured-list-link-color;
- }
- .header when (@is-overleaf = true) {
- font-weight: 600;
- }
+ list-style-type: none;
+ margin: 0;
+ overflow: hidden;
+ overflow-y: auto;
+ -ms-overflow-style: -ms-autohiding-scrollbar;
+ li {
+ border-bottom: 1px solid @structured-list-border-color;
+ padding: (@line-height-computed / 4) 0;
- .header when (@is-overleaf = false) {
- text-transform: uppercase;
- }
-
- .select-item, .select-all {
- position: absolute;
- left: @line-height-computed;
- }
- .select-item + span, .select-all + span {
- display: inline-block;
- padding-left: @line-height-computed * 1.5;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: top;
- }
- }
+ &:last-child {
+ border-bottom: 0 none;
+ }
+ &:hover {
+ background-color: @structured-list-hover-color;
+ }
+ &:first-child {
+ border-bottom-color: @structured-header-border-color;
+ &:hover {
+ background-color: transparent;
+ }
+ }
+ a {
+ color: @structured-list-link-color;
+ }
+ .header when (@is-overleaf = true) {
+ font-weight: 600;
+ }
+
+ .header when (@is-overleaf = false) {
+ text-transform: uppercase;
+ }
+
+ .select-item,
+ .select-all {
+ position: absolute;
+ left: @line-height-computed;
+ }
+ .select-item + span,
+ .select-all + span {
+ display: inline-block;
+ padding-left: @line-height-computed * 1.5;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: top;
+ }
+ }
}
.project-list-card when (@is-overleaf) {
- padding: 0 (@line-height-computed / 4);
+ padding: 0 (@line-height-computed / 4);
}
ul.project-list {
- li {
- .last-modified when (@is-overleaf = false) {
- font-size: .8rem;
- }
- .owner when (@is-overleaf = false) {
- font-size: .8rem;
- }
- .owner when (@is-overleaf = false) {
- margin-right: 0;
- }
- .projectName {
- margin-right: @line-height-computed / 4;
- padding: 0;
- vertical-align: inherit;
- white-space: normal;
- text-align: left;
- }
+ li {
+ .last-modified when (@is-overleaf = false) {
+ font-size: 0.8rem;
+ }
+ .owner when (@is-overleaf = false) {
+ font-size: 0.8rem;
+ }
+ .owner when (@is-overleaf = false) {
+ margin-right: 0;
+ }
+ .projectName {
+ margin-right: @line-height-computed / 4;
+ padding: 0;
+ vertical-align: inherit;
+ white-space: normal;
+ text-align: left;
+ }
- .v1-badge {
- margin-left: -4px;
- }
+ .v1-badge {
+ margin-left: -4px;
+ }
- .action-btn {
- padding: 0 0.3em;
- margin-left: 0.2em;
- }
- }
- i.tablesort {
- padding-left: 8px;
- }
+ .action-btn {
+ padding: 0 0.3em;
+ margin-left: 0.2em;
+ }
+ }
+ i.tablesort {
+ padding-left: 8px;
+ }
}
.tag-label {
- margin-left: @line-height-computed / 4;
- position: relative;
- display: inline-block;
- white-space: nowrap;
- top: @tag-top-adjustment;
+ margin-left: @line-height-computed / 4;
+ position: relative;
+ display: inline-block;
+ white-space: nowrap;
+ top: @tag-top-adjustment;
+}
+// Extra specificity needed to override Bootstrap's own specificity.
+.label.tag-label-name,
+.label.tag-label-remove {
+ display: inline-block;
+ padding: 3px 4px;
+ border-radius: @tag-border-radius;
+ background-color: @tag-bg-color;
+ color: @tag-color;
+ vertical-align: text-bottom;
+ border-width: 0;
+ &:hover,
+ &:focus {
+ color: @tag-color;
+ background-color: @tag-bg-hover-color;
+ outline-width: 0;
+ }
+}
+.label.tag-label-name {
+ padding-right: 2px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ max-width: @tag-max-width;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ > .fa {
+ margin-right: 0.3em;
+ }
+}
+.label.tag-label-remove {
+ padding-left: 2px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
}
- // Extra specificity needed to override Bootstrap's own specificity.
- .label.tag-label-name,
- .label.tag-label-remove {
- display: inline-block;
- padding: 3px 4px;
- border-radius: @tag-border-radius;
- background-color: @tag-bg-color;
- color: @tag-color;
- vertical-align: text-bottom;
- border-width: 0;
- &:hover,
- &:focus {
- color: @tag-color;
- background-color: @tag-bg-hover-color;
- outline-width: 0;
- }
- }
- .label.tag-label-name {
- padding-right: 2px;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- max-width: @tag-max-width;
- overflow: hidden;
- text-overflow: ellipsis;
- > .fa {
- margin-right: 0.3em;
- }
- }
- .label.tag-label-remove {
- padding-left: 2px;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- }
-
.user_details_auto_complete {
+ ul > li {
+ list-style: none;
+ }
- ul>li{
- list-style:none;
- }
+ .autocomplete {
+ width: 100%;
+ position: relative;
+ }
- .autocomplete {
- width: 100%;
- position: relative;
- }
+ .autocomplete ul {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: @zindex-dropdown;
+ float: left;
+ min-width: 160px;
+ padding: 5px 0;
+ margin: 2px 0 0; // override default ul
+ list-style: none;
+ font-size: @font-size-base;
+ background-color: @dropdown-bg;
+ border: 1px solid @dropdown-fallback-border; // IE8 fallback
+ border: 1px solid @dropdown-border;
+ border-radius: @border-radius-base;
+ .box-shadow(0 6px 12px rgba(0, 0, 0, 0.175));
+ background-clip: padding-box;
- .autocomplete ul {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: @zindex-dropdown;
- float: left;
- min-width: 160px;
- padding: 5px 0;
- margin: 2px 0 0; // override default ul
- list-style: none;
- font-size: @font-size-base;
- background-color: @dropdown-bg;
- border: 1px solid @dropdown-fallback-border; // IE8 fallback
- border: 1px solid @dropdown-border;
- border-radius: @border-radius-base;
- .box-shadow(0 6px 12px rgba(0,0,0,.175));
- background-clip: padding-box;
+ // Links within the dropdown menu
+ > li {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: normal;
+ line-height: @line-height-base;
+ color: @dropdown-link-color;
+ white-space: nowrap; // prevent links from randomly breaking onto new lines
+ }
- // Links within the dropdown menu
- > li {
- display: block;
- padding: 3px 20px;
- clear: both;
- font-weight: normal;
- line-height: @line-height-base;
- color: @dropdown-link-color;
- white-space: nowrap; // prevent links from randomly breaking onto new lines
- }
-
- > li.active {
- text-decoration: none;
- color: @dropdown-link-hover-color;
- background-color: @dropdown-link-hover-bg;
- }
- }
- .autocomplete .highlight {
- font-weight: 700;
- }
+ > li.active {
+ text-decoration: none;
+ color: @dropdown-link-hover-color;
+ background-color: @dropdown-link-hover-bg;
+ }
+ }
+ .autocomplete .highlight {
+ font-weight: 700;
+ }
}
.minimal-create-proj-dropdown {
- text-align:center;
+ text-align: center;
- &-menu {
- width:200px;
- left:50%;
- margin-left:-100px;
- }
+ &-menu {
+ width: 200px;
+ left: 50%;
+ margin-left: -100px;
+ }
}
.announcements {
- position: absolute;
- bottom: @footer-height;
- right: 0;
- height: 150px;
- width: 100%;
- pointer-events: none;
- overflow: hidden;
+ position: absolute;
+ bottom: @footer-height;
+ right: 0;
+ height: 150px;
+ width: 100%;
+ pointer-events: none;
+ overflow: hidden;
- &-open {
- top: -100%;
- height: auto;
- pointer-events: all;
- }
+ &-open {
+ top: -100%;
+ height: auto;
+ pointer-events: all;
+ }
}
.announcements-backdrop {
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- background-color: rgba(0, 0, 0, 0.35);
- opacity: 0;
- animation: fade-in 0.35s forwards;
- z-index: 1;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.35);
+ opacity: 0;
+ animation: fade-in 0.35s forwards;
+ z-index: 1;
}
.announcements-btn {
- position: absolute;
- bottom: -35px;
- right: 3%;
- width: 80px;
- height: 80px;
- background: url(/img/brand/lion.svg) no-repeat center/80% transparent;
- border-radius: 50%;
- box-shadow: none;
- z-index: 1;
- pointer-events: all;
- transition: bottom 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55),
- background 0.25s ease,
- box-shadow 0.25s ease;
+ position: absolute;
+ bottom: -35px;
+ right: 3%;
+ width: 80px;
+ height: 80px;
+ background: url(/img/brand/lion.svg) no-repeat center/80% transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ z-index: 1;
+ pointer-events: all;
+ transition: bottom 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55),
+ background 0.25s ease, box-shadow 0.25s ease;
- &:hover {
- bottom: -25px;
- }
+ &:hover {
+ bottom: -25px;
+ }
- &-open, &-open:hover,
- &-has-new, &-has-new:hover {
- background-color: #FFF;
- box-shadow: @announcements-shadow;
- bottom: 30px;
- }
+ &-open,
+ &-open:hover,
+ &-has-new,
+ &-has-new:hover {
+ background-color: #fff;
+ box-shadow: @announcements-shadow;
+ bottom: 30px;
+ }
+}
+.announcements-badge {
+ display: inline-block;
+ position: absolute;
+ font-size: 11px;
+ height: 1.8em;
+ min-width: 1.8em;
+ border-radius: 0.9em;
+ line-height: 1.8;
+ padding: 0 2px;
+ top: 1px;
+ right: 1px;
+ font-weight: bold;
+ color: #fff;
+ background-color: @red;
+ vertical-align: baseline;
+ white-space: nowrap;
+ text-align: center;
+ z-index: 1;
+ animation: pulse 1s alternate infinite;
}
- .announcements-badge {
- display: inline-block;
- position: absolute;
- font-size: 11px;
- height: 1.8em;
- min-width: 1.8em;
- border-radius: 0.9em;
- line-height: 1.8;
- padding: 0 2px;
- top: 1px;
- right: 1px;
- font-weight: bold;
- color: #FFF;
- background-color: @red;
- vertical-align: baseline;
- white-space: nowrap;
- text-align: center;
- z-index: 1;
- animation: pulse 1s alternate infinite;
- }
.announcements-body {
- display: flex;
- flex-direction: column;
- align-items: stretch;
- position: absolute;
- right: 3%;
- margin-right: 95px;
- bottom: 30px;
- width: 700px;
- max-height: 40%;
- min-height: 100px;
- background: #FFF;
- z-index: 1;
- box-shadow: @announcements-shadow;
- border-radius: @border-radius-base;
- animation: fade-in 0.35s forwards;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ position: absolute;
+ right: 3%;
+ margin-right: 95px;
+ bottom: 30px;
+ width: 700px;
+ max-height: 40%;
+ min-height: 100px;
+ background: #fff;
+ z-index: 1;
+ box-shadow: @announcements-shadow;
+ border-radius: @border-radius-base;
+ animation: fade-in 0.35s forwards;
- &::after {
- content: "\25b8";
- position: absolute;
- left: 100%;
- bottom: 17px;
- width: 30px;
- color: #FFF;
- text-shadow: @announcements-shadow;
- font-size: 2em;
- overflow: hidden;
- text-indent: -7px;
- }
+ &::after {
+ content: '\25b8';
+ position: absolute;
+ left: 100%;
+ bottom: 17px;
+ width: 30px;
+ color: #fff;
+ text-shadow: @announcements-shadow;
+ font-size: 2em;
+ overflow: hidden;
+ text-indent: -7px;
+ }
}
- .announcements-scroller {
- padding: @line-height-computed;
- flex-grow: 0;
- overflow-x: hidden;
- overflow-y: auto;
- }
- .announcement {
- margin-bottom: @line-height-computed * 1.5;
- &:last-child {
- margin-bottom: 0;
- }
- }
- .announcement-header {
- .page-header;
- margin: 0;
- }
+.announcements-scroller {
+ padding: @line-height-computed;
+ flex-grow: 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+.announcement {
+ margin-bottom: @line-height-computed * 1.5;
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+.announcement-header {
+ .page-header;
+ margin: 0;
+}
- .announcement-description {
- margin: (@line-height-computed / 4) 0 (@line-height-computed / 2);
- }
+.announcement-description {
+ margin: (@line-height-computed / 4) 0 (@line-height-computed / 2);
+}
- .announcement-meta {
- .clearfix;
- font-size: 0.9em;
- }
+.announcement-meta {
+ .clearfix;
+ font-size: 0.9em;
+}
- .announcement-date {
- float: left;
- color: @gray;
- margin: 0;
- }
+.announcement-date {
+ float: left;
+ color: @gray;
+ margin: 0;
+}
- .announcement-link {
- float: right;
- margin: 0;
- }
+.announcement-link {
+ float: right;
+ margin: 0;
+}
diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js
index 1a2dcd8b1f..db524ed3e7 100644
--- a/services/web/test/unit/src/Project/ProjectControllerTests.js
+++ b/services/web/test/unit/src/Project/ProjectControllerTests.js
@@ -17,8 +17,10 @@ describe('ProjectController', function() {
this.user = {
_id: ObjectId('123456123456123456123456'),
+ email: 'test@overleaf.com',
first_name: 'bjkdsjfk',
- features: {}
+ features: {},
+ emails: [{ email: 'test@overleaf.com' }]
}
this.settings = {
apis: {
@@ -107,13 +109,27 @@ describe('ProjectController', function() {
fire: sinon.stub()
}
}
- this.Features = { hasFeature: sinon.stub() }
+ this.Features = {
+ hasFeature: sinon
+ .stub()
+ .withArgs('saml')
+ .returns(false)
+ }
this.BrandVariationsHandler = {
getBrandVariationById: sinon
.stub()
.callsArgWith(1, null, this.brandVariationDetails)
}
- this.getUserAffiliations = sinon.stub().callsArgWith(1, null, [])
+ this.getUserAffiliations = sinon.stub().callsArgWith(1, null, [
+ {
+ email: 'test@overleaf.com',
+ institution: {
+ id: 1,
+ name: 'Overleaf',
+ ssoEnabled: true
+ }
+ }
+ ])
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
globals: {
@@ -622,6 +638,99 @@ describe('ProjectController', function() {
this.ProjectController.projectListPage(this.req, this.res)
})
})
+
+ describe('When Institution SSO is released', function() {
+ beforeEach(function(done) {
+ this.institutionEmail = 'test@overleaf.com'
+ this.institutionName = 'Overleaf'
+ this.Features.hasFeature.withArgs('saml').returns(true)
+ done()
+ })
+ it('should show institution SSO available notification', function() {
+ this.res.render = (pageName, opts) => {
+ expect(opts.notifications).to.deep.include({
+ email: 'test@overleaf.com',
+ institutionId: 1,
+ institutionName: 'Overleaf',
+ templateKey: 'notification_institution_sso_available'
+ })
+ }
+ this.ProjectController.projectListPage(this.req, this.res)
+ })
+ it('should show a linked notification', function() {
+ this.req.session.saml = {
+ institutionEmail: this.institutionEmail,
+ linked: {
+ hasEntitlement: false,
+ universityName: this.institutionName
+ }
+ }
+ this.res.render = (pageName, opts) => {
+ expect(opts.notifications).to.deep.include({
+ email: this.institutionEmail,
+ institutionName: this.institutionName,
+ templateKey: 'notification_institution_sso_linked'
+ })
+ }
+ this.ProjectController.projectListPage(this.req, this.res)
+ })
+ it('should show a linked another email notification', function() {
+ // when they request to link an email but the institution returns
+ // a different email
+ this.res.render = (pageName, opts) => {
+ expect(opts.notifications).to.deep.include({
+ institutionEmail: this.institutionEmail,
+ requestedEmail: 'requested@overleaf.com',
+ templateKey: 'notification_institution_sso_non_canonical'
+ })
+ }
+ this.req.session.saml = {
+ emailNonCanonical: this.institutionEmail,
+ institutionEmail: this.institutionEmail,
+ requestedEmail: 'requested@overleaf.com',
+ linked: {
+ hasEntitlement: false,
+ universityName: this.institutionName
+ }
+ }
+ this.ProjectController.projectListPage(this.req, this.res)
+ })
+ it('should show a notification when intent was to register via SSO but account existed', function() {
+ this.res.render = (pageName, opts) => {
+ expect(opts.notifications).to.deep.include({
+ email: this.institutionEmail,
+ templateKey: 'notification_institution_sso_already_registered'
+ })
+ }
+ this.req.session.saml = {
+ institutionEmail: this.institutionEmail,
+ linked: {
+ hasEntitlement: false,
+ universityName: 'Overleaf'
+ },
+ registerIntercept: true
+ }
+ this.ProjectController.projectListPage(this.req, this.res)
+ })
+ })
+
+ describe('When Institution SSO is not released', function() {
+ beforeEach(function(done) {
+ this.Features.hasFeature.withArgs('saml').returns(false)
+ done()
+ })
+ it('should not show institution sso available notification', function() {
+ this.res.render = (pageName, opts) => {
+ expect(opts.notifications).to.deep.not.include({
+ email: 'test@overleaf.com',
+ institutionId: 1,
+ institutionName: 'Overleaf',
+ templateKey: 'notification_institution_sso_available'
+ })
+ }
+ this.ProjectController.projectListPage(this.req, this.res)
+ })
+ })
})
describe('projectListPage with duplicate projects', function() {