') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Offline sign-in and registration error messages by micahscopes · Pull Request #2940 · learningequality/studio · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@
<Banner :value="!valid" error class="mb-4">

@radinamaticradinamaticMay 27, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this alert be moved on top, right after the main heading Create an account, same as it is on the sign in page, right after the Kolibri Studio heading?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe they need to restart the browser or the device to get the connection back, and there's nothing more annoying than having to fill in the same (long) form again... 😒

@khangmachkhangmachMay 29, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to echo some of Radina's thoughts above, I'd also prefer the alert moved after the 'create an account' heading and remove the offline icon indicator. The registration form on Studio is fairly long and there's a change the user might miss the offline indicator regardless if they're viewing the top or bottom portion of the screen.

Ideally I'd like to repurpose and tweak this behavior in Kolibri for these two offline cases:

  • Repurpose sticky behavior of the snackbars below, so wherever the user is on the form, the error is visible
  • Remove the dark scrim background so the user can still input information into the text fields but wait until their connection is restored to submit
  • Change string on snackbar 1 to something suggested like 'You seem to be offline. Please connect to the internet before finishing registration'

Screen Shot 2021-05-28 at 6 07 56 PM

Screen Shot 2021-05-28 at 6 08 50 PM

Let me know if this is possible, @micahscopes and I'll mock something up for it!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the proposed sticky snackbar, good strategy to make the alert visible independent of where the user is along the form.

Also, as I suggested, let's rephrase the string to You seem to be offline. Please connect to the internet to create an account. to avoid introducing the the term registration.

@micahscopesmicahscopesJun 1, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback @radinamatic and @khangmach.

When I first got into this, a snackbar seemed ideal, but using the actual snackbar components appeared to be tricky at the time, just because these login pages aren't part of the same Vue app as the rest of Studio.

That said, I'm glad to take another look at that. In the meantime, could simply moving the message up to the top be a reasonable starting point to get the basic functionality in for now, with the intention of iterating and switching to snackbars going forward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@micahscopes yep we can do that in the meantime.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏽 from me too, just correct the strings so we can have one thing less to think about!

{{ registrationFailed ? $tr('registrationFailed') : $tr('errorsMessage') }}
</Banner>

<Banner :value="offline" error class="mb-4">
{{ $tr('registrationFailedOffline') }}
</Banner>
<!-- Basic information -->
<h1 class="font-weight-bold my-2 subheading">
{{ $tr('basicInformationHeader') }}
Expand DownExpand Up@@ -157,7 +159,7 @@
<p class="mb-4">
{{ $tr('contactMessage') }}
</p>
<VBtn color="primary" large type="submit">
<VBtn color="primary" large :disabled="offline" type="submit">
{{ $tr('finishButton') }}
</VBtn>
</VForm>
Expand All@@ -171,7 +173,7 @@

<script>

import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import { uses, sources } from '../constants';
import TextField from 'shared/views/form/TextField';
import EmailField from 'shared/views/form/EmailField';
Expand DownExpand Up@@ -222,6 +224,9 @@
};
},
computed: {
...mapState({
offline: state => !state.connection.online,
}),
...mapGetters('policies', ['getPolicyAcceptedData']),
passwordConfirmRules() {
return [value => (this.form.password1 === value ? true : this.$tr('passwordMatchMessage'))];
Expand DownExpand Up@@ -410,7 +415,9 @@
this.$router.push({ name: 'ActivationSent' });
})
.catch(error => {
if (error.response.status === 403) {
if (error.message === 'Network Error') {
this.$refs.top.scrollIntoView({ behavior: 'smooth' });
} else if (error.response.status === 403) {
this.emailErrors = [this.$tr('emailExistsMessage')];
} else if (error.response.status === 405) {
this.$router.push({ name: 'AccountNotActivated' });
Expand All@@ -431,6 +438,8 @@
fieldRequiredMessage: 'Field is required',
errorsMessage: 'Please fix the errors below',
registrationFailed: 'There was an error registering your account. Please try again',
registrationFailedOffline:
'You seem to be offline. Please connect to the internet to create an account.',

// Basic information strings
basicInformationHeader: 'Basic information',
Expand Down
31 changes: 27 additions & 4 deletions contentcuration/contentcuration/frontend/accounts/pages/Main.vue
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,9 @@
justify-center
class="main pt-5"
>
<h3 class="corner">
<OfflineText indicator />
</h3>
<div>
<!-- Sign in -->
<VCard class="pa-4" style="width: 300px;margin: 0 auto;">
Expand All@@ -20,6 +23,7 @@
{{ $tr('kolibriStudio') }}
</h2>
<Banner :value="loginFailed" :text="$tr('loginFailed')" error />
<Banner :value="offline" :text="$tr('loginFailedOffline')" error />
<Banner
:value="Boolean(nextParam)"
:text="$tr('loginToProceed')"
Expand All@@ -32,7 +36,7 @@
<p>
<ActionLink :to="{ name: 'ForgotPassword' }" :text="$tr('forgotPasswordLink')" />
</p>
<VBtn block color="primary" large type="submit" :disabled="busy">
<VBtn block color="primary" large type="submit" :disabled="offline || busy">
{{ $tr('signInButton') }}
</VBtn>
<VBtn block flat color="primary" class="mt-2" :to="{ name: 'Create' }">
Expand DownExpand Up@@ -79,13 +83,14 @@

<script>

import { mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import EmailField from 'shared/views/form/EmailField';
import PasswordField from 'shared/views/form/PasswordField';
import Banner from 'shared/views/Banner';
import PolicyModals from 'shared/views/policies/PolicyModals';
import { policies } from 'shared/constants';
import LanguageSwitcherList from 'shared/languageSwitcher/LanguageSwitcherList';
import OfflineText from 'shared/views/OfflineText';

export default {
name: 'Main',
Expand All@@ -95,16 +100,21 @@
LanguageSwitcherList,
PasswordField,
PolicyModals,
OfflineText,
},
data() {
return {
username: '',
password: '',
loginFailed: false,
busy: false,
loginFailedOffline: false,
};
},
computed: {
...mapState({
offline: state => !state.connection.online,
}),
nextParam() {
const params = new URLSearchParams(window.location.search.substring(1));
return params.get('next');
Expand All@@ -128,14 +138,19 @@
};
return this.login(credentials)
.then(() => {
this.loginFailedOffline = false;
this.loginFailed = false;
window.location.assign(this.nextParam || '/channels');
})
.catch(err => {
this.busy = false;
if (err.response.status === 405) {
if (err.message === 'Network Error') {
this.loginFailedOffline = true;
} else if (err.response.status === 405) {
this.$router.push({ name: 'AccountNotActivated' });
} else {
this.loginFailed = true;
}
this.loginFailed = true;
});
}
return Promise.resolve();
Expand All@@ -153,6 +168,8 @@
TOSLink: 'Terms of service',
copyright: '© {year} Learning Equality',
loginToProceed: 'You must sign in to view that page',
loginFailedOffline:
'You seem to be offline. Please connect to the internet before signing in.',
},
};

Expand All@@ -174,4 +191,10 @@
content: '•';
}

.corner {
position: absolute;
top: 1em;
left: 1em;
}

</style>
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,16 @@ import router from '../../router';
import { uses, sources } from '../../constants';
import Create from '../Create';

const connectionStateMocks = {
$store: {
state: {
connection: {
offline: true,
},
},
},
};

const defaultData = {
first_name: 'Test',
last_name: 'User',
Expand DownExpand Up@@ -34,6 +44,7 @@ function makeWrapper(formData) {
},
},
stubs: ['PolicyModals'],
mocks: connectionStateMocks,
});
wrapper.setData({
form: {
Expand DownExpand Up@@ -87,7 +98,7 @@ describe('create', () => {
});
it('should automatically fill the email if provided in the query param', () => {
router.push({ name: 'Create', query: { email: 'newtest@test.com' } });
let wrapper = mount(Create, { router, stubs: ['PolicyModals'] });
let wrapper = mount(Create, { router, stubs: ['PolicyModals'], mocks: connectionStateMocks });
expect(wrapper.vm.form.email).toBe('newtest@test.com');
});
describe('validation', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,15 @@ function makeWrapper() {
let wrapper = mount(Main, {
router,
stubs: ['GlobalSnackbar', 'PolicyModals'],
mocks: {
$store: {
state: {
connection: {
online: true,
},
},
},
},
});
wrapper.setMethods({
login: data => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@
</span>
</div>
</template>
<span>{{ $tr('offlineText') }}</span>
<span>{{ offlineText || $tr('offlineText') }}</span>
</VTooltip>
<ToolBar
v-else-if="toolbar"
Expand All@@ -29,13 +29,13 @@
<Icon class="mx-3">
cloud_off
</Icon>
<span>{{ $tr('offlineText') }}</span>
<span>{{ offlineText || $tr('offlineText') }}</span>
</ToolBar>
<div v-else>
<Icon class="mx-3">
cloud_off
</Icon>
<span>{{ $tr('offlineText') }}</span>
<span>{{ offlineText || $tr('offlineText') }}</span>
</div>
</template>
</VFadeTransition>
Expand DownExpand Up@@ -65,6 +65,10 @@
type: Number,
default: 0,
},
offlineText: {
type: String,
default: null,
},
},
computed: {
...mapState({
Expand Down