Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dialog to announce new features in the UI #4814

Merged
merged 13 commits into from
Jan 23, 2024
2 changes: 1 addition & 1 deletion jsapp/js/account/accountSettingsRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const AccountSettings = observer(() => {
<div className='anonymous-submission-notice-copy'>
<strong>
{t(
'"Require authentication to see forms and submit data" has been moved.'
'You can now control whether to allow anonymous submissions in web forms for each project. Previously, this was an account-wide setting.'
)}
</strong>
&nbsp;
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/components/anonymousSubmission.component.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React from 'react';
import ToggleSwitch from 'js/components/common/toggleSwitch';
import envStore from 'js/envStore';
import {HELP_ARTICLE_ANON_SUBMISSIONS_URL} from 'js/constants';
Expand All @@ -17,7 +17,7 @@ export default function AnonymousSubmission(props: AnonymousSubmissionProps) {
checked={props.checked}
onChange={props.onChange}
label={t(
'Allow web submissions to this form without a username and password'
'Allow submissions to this form without a username and password'
)}
/>
<a
Expand Down
32 changes: 25 additions & 7 deletions jsapp/js/components/formLanding.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import sessionStore from 'js/stores/session';
import PopoverMenu from 'js/popoverMenu';
import LoadingSpinner from 'js/components/common/loadingSpinner';
import InlineMessage from 'js/components/common/inlineMessage';
import Icon from 'js/components/common/icon';
import mixins from '../mixins';
import {actions} from '../actions';
import DocumentTitle from 'react-document-title';
Expand All @@ -28,9 +27,10 @@ import {
} from 'js/components/permissions/utils';
import permConfig from 'js/components/permissions/permConfig';
import {PERMISSIONS_CODENAMES} from 'js/components/permissions/permConstants';
import ToggleSwitch from 'js/components/common/toggleSwitch';
import {HELP_ARTICLE_ANON_SUBMISSIONS_URL} from 'js/constants';
import AnonymousSubmission from './anonymousSubmission.component';
import styles from './anonymousSubmission.module.scss';
import NewFeatureDialog from './newFeatureDialog.component';

const DVCOUNT_LIMIT_MINIMUM = 20;
const ANON_CAN_ADD_PERM_URL = permConfig.getPermissionByCodename(
Expand All @@ -47,6 +47,7 @@ class FormLanding extends React.Component {
nextPagesVersions: [],
anonymousSubmissions: false,
anonymousPermissions: [],
shouldShowNewFeatureDialog: false,
};
autoBind(this);
}
Expand Down Expand Up @@ -81,7 +82,9 @@ class FormLanding extends React.Component {
const permission = this.state.anonymousPermissions.find(
(perm) =>
perm.permission ===
permConfig.getPermissionByCodename(PERMISSIONS_CODENAMES.add_submissions).url
permConfig.getPermissionByCodename(
PERMISSIONS_CODENAMES.add_submissions
).url
);
if (this.state.anonymousSubmissions) {
actions.permissions.removeAssetPermission(
Expand Down Expand Up @@ -170,6 +173,9 @@ class FormLanding extends React.Component {
}
showSharingModal(evt) {
evt.preventDefault();
stores.pageState._onHideModal = () => {
this.setState({shouldShowNewFeatureDialog: true});
};
stores.pageState.showModal({
type: MODAL_TYPES.SHARING,
assetid: this.state.uid,
Expand Down Expand Up @@ -247,6 +253,7 @@ class FormLanding extends React.Component {
});
}
}

renderHistory() {
var dvcount = this.state.deployed_versions.count;
const versionsToDisplay = this.state.deployed_versions.results.concat(
Expand Down Expand Up @@ -425,10 +432,21 @@ class FormLanding extends React.Component {
<bem.FormView__cell
m={['padding', 'anonymous-submissions', 'bordertop']}
>
<AnonymousSubmission
checked={this.state.anonymousSubmissions}
onChange={() => this.updateAssetAnonymousSubmissions()}
/>
<NewFeatureDialog
content={t(
'You can now control whether to allow anonymous submissions for each project. Previously, this was an account-wide setting.'
)}
supportArticle={
envStore.data.support_url + HELP_ARTICLE_ANON_SUBMISSIONS_URL
}
featureKey='anonymousSubmissions'
disabled={stores.pageState.state?.modal}
>
<AnonymousSubmission
checked={this.state.anonymousSubmissions}
onChange={() => this.updateAssetAnonymousSubmissions()}
/>
</NewFeatureDialog>
</bem.FormView__cell>
)}
</bem.FormView__cell>
Expand Down
113 changes: 113 additions & 0 deletions jsapp/js/components/newFeatureDialog.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, {useState, useEffect} from 'react';
import Button from 'js/components/common/button';
import sessionStore from 'js/stores/session';
import styles from './newFeatureDialog.module.scss';
import cx from 'classnames';

interface NewFeatureDialogProps {
children: React.ReactNode;
className?: string;
/**
* Used to differentiate between dialogs for different features.
* Tip: Use the feature name. It's added to the end of the localstorage key.
* If two or more dialogs have the same featureKey, clicking one should dismiss all of them
* for the current and future sessions of the presently logged-in user.
*/
featureKey: string;
content: string;
supportArticle?: string;
/**
* Manually disable the dialog. Useful if there are more than one for the
* same feature on screen.
*/
disabled?: boolean;
}

export default function NewFeatureDialog({
children,
className = '',
featureKey,
content,
supportArticle,
disabled = false,
}: NewFeatureDialogProps) {
const [showDialog, setShowDialog] = useState<boolean>(false);
const [localStorageKey, setLocalStorageKey] = useState('');

/*
* When this component is mounted, create the localstorage key we'll use to
* store/check whether the dialog has been dismissed
*/
useEffect(() => {
(async () => {
const username = sessionStore.currentAccount.username;
if (crypto.subtle) {
// Let's avoid leaving behind an easily-accessible list of all users
// who've logged in with this browser
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
const encoder = new TextEncoder();
const encoded = encoder.encode(username);
const hashBuffer = await crypto.subtle.digest('SHA-256', encoded);
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join(''); // convert bytes to hex string
setLocalStorageKey(`kpiDialogStatus-${featureKey}-${hashHex}`);
} else {
// `crypto.subtle` is only available in secure (https://) contexts
setLocalStorageKey(
`kpiDialogStatus-${featureKey}-FOR DEVELOPMENT ONLY-${username}`
);
}
})();
}, []);

/*
* Show the dialog if we have a key to check and localstorage has an entry for this
* user/feature combination, hide it otherwise
*/
useEffect(() => {
const dialogStatus =
localStorageKey && localStorage.getItem(localStorageKey);
setShowDialog(!dialogStatus);
}, [disabled, localStorageKey]);

// Close the dialog box and store that we've closed it
function closeDialog() {
localStorage.setItem(localStorageKey, 'shown');
setShowDialog(false);
}

return (
<div className={cx(styles.root, {className: className})}>
<div className={styles.wrapper}>{children}</div>
{showDialog && !disabled && (
<div className={styles.dialog}>
<div className={styles.header}>
{t('New feature')}
<Button
duvld marked this conversation as resolved.
Show resolved Hide resolved
color='dark-blue'
size='s'
type='full'
startIcon='close'
onClick={closeDialog}
/>
</div>
<div className={styles.content}>
{content}
&nbsp;
{supportArticle && (
<a
href={supportArticle}
target='_blank'
className={styles.support}
>
{t('Learn more')}
</a>
)}
</div>
</div>
)}
</div>
);
}
54 changes: 54 additions & 0 deletions jsapp/js/components/newFeatureDialog.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@use '~kobo-common/src/styles/colors';
@use 'scss/sizes';

.root {
position: relative;
z-index: 1100; // 1 index lower than a modal
}

.wrapper {
position: relative;
}

.dialog {
width: 50%;
position: absolute;
border-radius: sizes.$x4;
padding: sizes.$x12;
color: colors.$kobo-white;
background: colors.$kobo-dark-blue;
}

.dialog::before {
content: " ";
left: 5%;
border: solid transparent;
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-width: sizes.$x6;
margin-left: calc(sizes.$x6 * -1);
bottom: 100%;
border-bottom-color: colors.$kobo-dark-blue;
}

.header {
color: colors.$kobo-white;
font-weight: 600;
display: flex;
justify-content: space-between;
padding-bottom: sizes.$x12;
align-items: center;

.k-button :hover {
background-color: transparent;
}
}

.support {
padding-top: sizes.$x8;
margin: 0;
color: lighten(colors.$kobo-blue, 20%);
text-decoration: underline;
}
21 changes: 17 additions & 4 deletions jsapp/js/components/permissions/publicShareSettings.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import envStore from 'js/envStore';
import Icon from 'js/components/common/icon';
import ToggleSwitch from 'js/components/common/toggleSwitch';
import AnonymousSubmission from '../anonymousSubmission.component';
import styles from 'js/components/anonymousSubmission.module.scss';
import {stores} from 'js/stores';
import NewFeatureDialog from 'js/components/newFeatureDialog.component';

const HELP_ARTICLE_ANON_SUBMISSIONS_URL = 'managing_permissions.html';

Expand Down Expand Up @@ -71,10 +74,20 @@ class PublicShareSettings extends React.Component<PublicShareSettingsProps> {
return (
<bem.FormModal__item m='permissions'>
<bem.FormModal__item m='anonymous-submissions'>
<AnonymousSubmission
checked={anonCanAddData}
onChange={this.togglePerms.bind(this, 'add_submissions')}
/>
<NewFeatureDialog
content={t(
'You can now control whether to allow anonymous submissions for each project. Previously, this was an account-wide setting.'
)}
supportArticle={
envStore.data.support_url + HELP_ARTICLE_ANON_SUBMISSIONS_URL
}
featureKey='anonymousSubmissions'
>
<AnonymousSubmission
checked={anonCanAddData}
onChange={this.togglePerms.bind(this, 'add_submissions')}
/>
</NewFeatureDialog>
</bem.FormModal__item>

<bem.FormModal__item m='permissions-header'>
Expand Down
27 changes: 14 additions & 13 deletions jsapp/js/stores.d.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
interface PageStateModalParams {
type: string // one of MODAL_TYPES.NEW_FORM
[name: string]: any
type: string; // one of MODAL_TYPES.NEW_FORM
[name: string]: any;
}

// TODO: either change whole `stores.es6` to `stores.ts` or crete a type
// definition for a store you need.
export namespace stores {
const tags: any
const surveyState: any
const assetSearch: any
const translations: any
const tags: any;
const surveyState: any;
const assetSearch: any;
const translations: any;
const pageState: {
state: {
assetNavExpanded: boolean;
showFixedDrawer: boolean;
modal?: {} | false;
};
toggleFixedDrawer: () => void;
showModal: (params: PageStateModalParams) => void;
hideModal: () => void;
switchModal: (params: PageStateModalParams) => void;
switchToPreviousModal: () => void;
hasPreviousModal: () => boolean;
}
const snapshots: any
};
const snapshots: any;
const session: {
listen: (clb: Function) => void;
currentAccount: AccountResponse
isAuthStateKnown: boolean
isLoggedIn: boolean
}
const allAssets: any
currentAccount: AccountResponse;
isAuthStateKnown: boolean;
isLoggedIn: boolean;
};
const allAssets: any;
}