Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 8d3d056
Author: Leszek <[email protected]>
Date:   Fri Jul 12 12:34:21 2024 +0200

    small code cleanup in submission modal

commit 347684c
Author: Leszek <[email protected]>
Date:   Fri Jul 12 12:33:32 2024 +0200

    fix submission modal not loading submission from next page

commit d4d2a00
Author: Leszek <[email protected]>
Date:   Thu Jul 11 10:19:14 2024 +0200

    rename few properties for better clarity

commit c65f33d
Author: Leszek <[email protected]>
Date:   Wed Jul 10 00:32:16 2024 +0200

    cleanup and comment Submission Modal code

    also cleanup duplicated submission flow a bit

commit 8eaae02
Author: Leszek <[email protected]>
Date:   Tue Jul 9 15:46:54 2024 +0200

    use classnames util in KoboModal

commit 417d66f
Author: Leszek <[email protected]>
Date:   Tue Jul 9 15:46:38 2024 +0200

    small cleanup of KoboSelect styles

commit 0d9546c
Author: Leszek <[email protected]>
Date:   Tue Jul 9 15:46:17 2024 +0200

    Move background audio handling into Submission Modal from Data Table

    to make it more independent

commit d85bd1f
Author: Leszek <[email protected]>
Date:   Tue Jul 9 15:33:31 2024 +0200

    remove unused types import

commit 457c564
Author: Leszek <[email protected]>
Date:   Tue Jul 9 15:33:13 2024 +0200

    KoboDropdown name prop is not optional anymore

commit a7b1839
Merge: bac5130 368e755
Author: RuthShryock <[email protected]>
Date:   Mon Jul 8 11:07:11 2024 -0400

    Merge pull request #4994 from kobotoolbox/nlp-usage-limit-block-frontend

    [TASK-461] Add modal(s) to NLP page when user is over limits

commit bac5130
Merge: a8e4243 0e21155
Author: Philip Edwards <[email protected]>
Date:   Thu Jul 4 11:48:09 2024 -0400

    Merge pull request #4992 from kobotoolbox/react-18-upgrade

    Upgrade to React 18

commit 0e21155
Author: Phil Edwards <[email protected]>
Date:   Thu Jul 4 11:03:19 2024 -0400

    Pin unused storybook dependency, removes EBADENGINE warning

    #4992 (comment)

commit a8e4243
Merge: fac7edf 71920e9
Author: Olivier Léger <[email protected]>
Date:   Thu Jul 4 09:50:12 2024 -0400

    Merge pull request #4988 from kobotoolbox/remove-constance-values-assigned-in-tests

    Refactor `constance` setting assignments in tests to use `override_config`

commit 7d3da93
Author: Leszek <[email protected]>
Date:   Thu Jul 4 14:13:51 2024 +0200

    fix edit library item button link

commit 4b844ae
Author: Leszek <[email protected]>
Date:   Thu Jul 4 13:59:10 2024 +0200

    fix _prepColumns on validation status change

commit d1eec97
Author: Leszek <[email protected]>
Date:   Thu Jul 4 13:55:31 2024 +0200

    fix _prepColumns on initial Data Table load

commit 368e755
Author: James Kiger <[email protected]>
Date:   Wed Jul 3 13:12:52 2024 -0400

    Remove extra button styling

commit fac7edf
Merge: 167f665 c234e4e
Author: Philip Edwards <[email protected]>
Date:   Wed Jul 3 12:42:11 2024 -0400

    Merge pull request #4991 from kobotoolbox/organizations-error-anon-user

    Retrieve organization data only for logged-in users to prevent error messages

commit 71920e9
Author: RuthShryock <[email protected]>
Date:   Wed Jul 3 10:45:12 2024 -0400

    keep direct imports at the top

commit a2355d4
Author: RuthShryock <[email protected]>
Date:   Wed Jul 3 09:57:40 2024 -0400

    fix formatting and use LazyJSONSerializable instead of json.dumps

commit 8396b20
Author: Phil Edwards <[email protected]>
Date:   Tue Jul 2 16:03:02 2024 -0400

    Remove bonus console.log

commit 3cfebca
Author: Phil Edwards <[email protected]>
Date:   Tue Jul 2 13:08:44 2024 -0400

    Pass tests that would fail locally after mocha / chai / deep-eql update

commit 28eb4cb
Author: Phil Edwards <[email protected]>
Date:   Tue Jul 2 11:21:06 2024 -0400

    Install react-query v5

    Pre-emptively install react-query, to make branch switching easier for
    upcoming PR reviews.

commit 3631296
Author: Phil Edwards <[email protected]>
Date:   Mon Jul 1 17:36:55 2024 -0400

    Use React's "automatic" runtime instead of "classic"

    - https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html
    - https://swc.rs/docs/configuration/compilation#jsctransformreactruntime

    > "Use `runtime: automatic` to use a JSX runtime module
      (e.g. `react/jsx-runtime` introduced in React 17)."

    (Note that with the new jsx transform, you can now use jsx/tsx in a file
    without importing React. TS knows this now, too. Feel free to take its
    suggestion.)

    (The $schema key added to .swcrc just adds better hints when editing the
    JSON config, it's unrelated to the runtime change.)

commit df1d722
Author: Phil Edwards <[email protected]>
Date:   Mon Jul 1 17:24:11 2024 -0400

    Remove react-hot-loader in favor of react-refresh

    - https://github.com/gaearon/react-hot-loader?tab=readme-ov-file#moving-towards-next-step
    - https://github.com/pmmmwh/react-refresh-webpack-plugin
      - using `swc-loader`

    > swc-loader will set the development option based on Webpack's mode
    > option. swc won't enable fast refresh when development is false.

commit f49b046
Author: Phil Edwards <[email protected]>
Date:   Mon Jul 1 17:15:42 2024 -0400

    Bump several build-related packages

commit 10bb3c0
Author: Phil Edwards <[email protected]>
Date:   Mon Jul 1 12:11:13 2024 -0400

    Validate 'change email' field

    If you click the 'change email' button without filling the email input,
    in prod it appears to do nothing; in dev there's a fullscreen error.

    We should probably show a toast in case of API error, but meanwhile I
    through in some light validation to disable the button until there's at
    least something in the text input.

commit 30862c5
Author: Phil Edwards <[email protected]>
Date:   Fri Jun 28 14:43:46 2024 -0400

    Opt-in to the React 18 root API

    https://react.dev/blog/2022/03/08/react-18-upgrade-guide#updates-to-client-rendering-apis

commit a008ee3
Author: James Kiger <[email protected]>
Date:   Tue Jul 2 08:20:34 2024 -0400

    Use enum for usage limit types

commit 2b4ac08
Author: James Kiger <[email protected]>
Date:   Tue Jul 2 07:08:40 2024 -0400

    Minor corrections

commit c234e4e
Author: RuthShryock <[email protected]>
Date:   Mon Jul 1 15:28:03 2024 -0400

    don't get organization data if the user is not logged in

commit 66c8b59
Author: Phil Edwards <[email protected]>
Date:   Fri Jun 28 13:39:48 2024 -0400

    Satisfy updated TS / react types.

commit 5c7a8d1
Author: Phil Edwards <[email protected]>
Date:   Thu Jun 27 18:15:20 2024 -0400

    Update react to v18, allowing semver updates

    (Also update webpack-dev-server and autoprefixer while we're here)

    This commit does not fully build yet; need:

    - Fix emerging TS errors, from types updates
    - Make sure react-refresh, etc. works
    - Make sure all react-based libraries work
    - See about this EBADENGINE >=16.17.0 in Storybook's dependencies
    - …and more.

    Note the react-table override: v6 -> v7 or v8 is a substantial rewrite
    with no upgrade guide. My hope is that, as with reflux, it was forwards-
    compatible with react v18, and we can upgrade it later.

    A variety of deprecation warnings appear when you do a fresh install
    without package-lock.json. Here's how to view them again:

        rm -rf ./node_modules
        npm cache clean -f    # superstitiously, to get resolved/integrity *
        npm install
                                    * (https://stackoverflow.com/a/75236714)

commit d47f8cc
Author: James Kiger <[email protected]>
Date:   Mon Jul 1 10:51:17 2024 -0400

    Styling

commit cfb38da
Author: James Kiger <[email protected]>
Date:   Mon Jul 1 08:48:09 2024 -0400

    Clean up handling of seconds-to-minutes conversion for usage/limits

commit d0305ba
Author: RuthShryock <[email protected]>
Date:   Fri Jun 28 17:23:36 2024 -0400

    use override_config in test cases where config values are directly assigned

commit 039566d
Author: James Kiger <[email protected]>
Date:   Fri Jun 28 13:47:54 2024 -0400

    Add modal for blocking frontend nlp usage
  • Loading branch information
magicznyleszek committed Jul 12, 2024
1 parent 0c32b6e commit ad38cb7
Show file tree
Hide file tree
Showing 41 changed files with 27,110 additions and 31,877 deletions.
3 changes: 2 additions & 1 deletion .swcrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "https://swc.rs/schema.json",
"sourceMaps": true,
"jsc": {
"parser": {
Expand All @@ -7,7 +8,7 @@
},
"transform": {
"react": {
"runtime": "classic",
"runtime": "automatic",
"refresh": true
}
}
Expand Down
4 changes: 2 additions & 2 deletions hub/tests/test_admin_validators.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from constance import config
from constance.test import override_config
from django.contrib.auth.models import User
from django.test import TestCase
from hub.admin.extend_user import validate_superuser_auth
from kobo.apps.accounts.mfa.models import MfaMethod


@override_config(SUPERUSER_AUTH_ENFORCEMENT=True)
class ValidateSuperuserMfaTest(TestCase):

def setUp(self):
config.SUPERUSER_AUTH_ENFORCEMENT = True
self.superuser = User.objects.create_superuser(
username='admin', password='adminpassword'
)
Expand Down
4 changes: 4 additions & 0 deletions jsapp/js/account/billingContextProvider.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import {
OrganizationContext,
useOrganization,
} from 'js/account/organizations/useOrganization.hook';
import sessionStore from 'js/stores/session';

export const BillingContextProvider = (props: {children: ReactNode}) => {
if (!sessionStore.isLoggedIn) {
return <>{props.children}</>;
}
const [organization, reloadOrg, orgStatus] = useOrganization();
const usage = useUsage(organization?.id);
const products = useProducts();
Expand Down
6 changes: 3 additions & 3 deletions jsapp/js/account/organizations/requireOrgOwner.component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, {ReactElement, Suspense, useContext, useEffect} from 'react';
import {RouteObject, useNavigate} from 'react-router-dom';
import React, {Suspense, useContext, useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import {OrganizationContext} from 'js/account/organizations/useOrganization.hook';
import LoadingSpinner from 'js/components/common/loadingSpinner';
import {ACCOUNT_ROUTES} from 'js/account/routes.constants';

interface Props {
children: RouteObject[] | undefined | ReactElement;
children: React.ReactNode;
redirect?: boolean;
}

Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/account/plans/billingButton.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import cx from 'classnames';
import type {ButtonProps} from 'js/components/common/button';
import Button from 'js/components/common/button';
import React from 'react';
import {button} from './billingButton.module.scss';
import styles from './billingButton.module.scss';

/**
* The base button component that's used on the Plans/Add-ons pages.
Expand All @@ -16,7 +16,7 @@ export default function BillingButton(props: Partial<ButtonProps>) {
color='blue'
size='l'
{...props}
className={cx([button, props.className])}
className={cx([styles.button, props.className])}
isFullWidth
/>
);
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/account/plans/plan.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ export default function Plan(props: PlanProps) {
const getSubscribedProduct = useCallback(getSubscriptionsForProductId, []);

const isSubscribedProduct = useCallback(
(product: SinglePricedProduct, quantity = null) => {
(product: SinglePricedProduct, quantity: number | undefined) => {
if (!product.price?.unit_amount && !hasActiveSubscription) {
return true;
}
Expand All @@ -356,7 +356,7 @@ export default function Plan(props: PlanProps) {
(subscription: SubscriptionInfo) =>
subscription.items[0].price.id === product.price.id &&
hasManageableStatus(subscription) &&
quantity &&
quantity !== undefined &&
quantity === subscription.quantity
);
}
Expand Down
4 changes: 4 additions & 0 deletions jsapp/js/account/security/email/emailSection.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export default function EmailSection() {
value={email.newEmail}
placeholder={t('Type new email address')}
onChange={onTextFieldChange.bind(onTextFieldChange)}
type='email'
/>

<Button
Expand All @@ -178,6 +179,9 @@ export default function EmailSection() {
color='blue'
type='frame'
onClick={setNewUserEmail.bind(setNewUserEmail, email.newEmail)}
// quick simple subtle email validation to avoid complete accidents
// a toast showing any API error feedback would be nicer
isDisabled={!/[^@]+@[^@]+\.[^@]+/.test(email.newEmail)}
/>
</form>
</div>
Expand Down
7 changes: 7 additions & 0 deletions jsapp/js/account/stripe.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ export enum PlanNames {
'ENTERPRISE' = 'Enterprise',
}

export enum UsageLimitTypes {
'STORAGE' = 'storage',
'SUBMISSION' = 'submission',
'TRANSCRIPTION' = 'automated transcription',
'TRANSLATION' = 'machine translation',
}

export enum Limits {
'unlimited' = 'unlimited',
}
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/account/usage/usage.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import UsageContainer, {
USAGE_CONTAINER_TYPE,
} from 'js/account/usage/usageContainer';
import envStore from 'js/envStore';
import {formatDate} from 'js/utils';
import {convertSecondsToMinutes, formatDate} from 'js/utils';
import styles from './usage.module.scss';
import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
import {ProductsContext} from '../useProducts.hook';
Expand Down Expand Up @@ -104,7 +104,7 @@ export default function Usage() {
nlpCharacterLimit: limits.nlp_character_limit,
nlpMinuteLimit:
typeof limits.nlp_seconds_limit === 'number'
? limits.nlp_seconds_limit / 60
? convertSecondsToMinutes(limits.nlp_seconds_limit)
: limits.nlp_seconds_limit,
submissionLimit: limits.submission_limit,
isLoaded: true,
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/account/usage/usageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ const UsageContainer = ({
/**
* Render a limit amount, usage amount, or total balance as readable text
* @param {number|'unlimited'} amount - The limit/usage amount
* @param {number|'unlimited'|null} [available=null] - If we're showing a balance,
* @param {number|'unlimited'} [available] - If we're showing a balance,
* `amount` takes the usage amount and this takes the limit amount
*/
const limitDisplay = useCallback(
(amount, available = null) => {
(amount: LimitAmount, available?: LimitAmount) => {
if (amount === Limits.unlimited || available === Limits.unlimited) {
return t('Unlimited');
}
Expand Down
14 changes: 5 additions & 9 deletions jsapp/js/account/usage/usageProjectBreakdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {USAGE_ASSETS_PER_PAGE} from 'jsapp/js/constants';
import SortableProjectColumnHeader from 'jsapp/js/projects/projectsTable/sortableProjectColumnHeader';
import type {ProjectFieldDefinition} from 'jsapp/js/projects/projectViews/constants';
import type {ProjectsTableOrder} from 'jsapp/js/projects/projectsTable/projectsTable';
import {truncateNumber} from 'jsapp/js/utils';
import {UsageContext, useUsage} from './useUsage.hook';
import {convertSecondsToMinutes} from 'jsapp/js/utils';
import {UsageContext} from './useUsage.hook';
import Button from 'js/components/common/button';
import Icon from 'js/components/common/icon';
import {OrganizationContext} from 'js/account/organizations/useOrganization.hook';
Expand Down Expand Up @@ -132,10 +132,8 @@ const ProjectBreakdown = () => {
`submission_count_current_${usage.trackingPeriod}`
].toLocaleString();

const periodASRSeconds = truncateNumber(
project[`nlp_usage_current_${usage.trackingPeriod}`]
.total_nlp_asr_seconds / 60,
1
const periodASRSeconds = convertSecondsToMinutes(
project[`nlp_usage_current_${usage.trackingPeriod}`].total_nlp_asr_seconds
).toLocaleString();

const periodMTCharacters =
Expand All @@ -154,9 +152,7 @@ const ProjectBreakdown = () => {
</Link>
</td>
<td>{project.submission_count_all_time.toLocaleString()}</td>
<td className={styles.currentMonth}>
{periodSubmissions}
</td>
<td className={styles.currentMonth}>{periodSubmissions}</td>
<td>{prettyBytes(project.storage_bytes)}</td>
<td>{periodASRSeconds}</td>
<td>{periodMTCharacters}</td>
Expand Down
10 changes: 4 additions & 6 deletions jsapp/js/account/usage/useUsage.hook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createContext, useCallback} from 'react';
import type {Organization, RecurringInterval} from 'js/account/stripe.types';
import {getSubscriptionInterval} from 'js/account/stripe.api';
import {formatRelativeTime, truncateNumber} from 'js/utils';
import {convertSecondsToMinutes, formatRelativeTime} from 'js/utils';
import {getUsage} from 'js/account/usage/usage.api';
import {useApiFetcher, withApiFetcher} from 'js/hooks/useApiFetcher.hook';

Expand Down Expand Up @@ -50,11 +50,9 @@ const loadUsage = async (
return {
storage: usage.total_storage_bytes,
submissions: usage.total_submission_count[`current_${trackingPeriod}`],
transcriptionMinutes: Math.floor(
truncateNumber(
usage.total_nlp_usage[`asr_seconds_current_${trackingPeriod}`] / 60
)
), // seconds to minutes
transcriptionMinutes: convertSecondsToMinutes(
usage.total_nlp_usage[`asr_seconds_current_${trackingPeriod}`]
),
translationChars:
usage.total_nlp_usage[`mt_characters_current_${trackingPeriod}`],
currentMonthStart: usage.current_month_start,
Expand Down
3 changes: 1 addition & 2 deletions jsapp/js/components/assetsTable/assetActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,7 @@ class AssetActionButtons extends React.Component<
}

getFormBuilderLink() {
let link =
'#' + ROUTES.EDIT_LIBRARY_ITEM.replace(':uid', this.props.asset.uid);
let link = ROUTES.EDIT_LIBRARY_ITEM.replace(':uid', this.props.asset.uid);

// when editing a child from within a collection page
// make sure the "Return to list" button goes back to collection
Expand Down
1 change: 0 additions & 1 deletion jsapp/js/components/bigModal/bigModal.es6
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,6 @@ class BigModal extends React.Component {
ids={this.props.params.ids}
isDuplicated={this.props.params.isDuplicated}
duplicatedSubmission={this.props.params.duplicatedSubmission}
backgroundAudioUrl={this.props.params.backgroundAudioUrl}
tableInfo={this.props.params.tableInfo || false}
/>
}
Expand Down
14 changes: 6 additions & 8 deletions jsapp/js/components/common/koboDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ interface KoboDropdownProps {
/** The content of dropdown, anything's allowed. */
menuContent: React.ReactNode;
/**
* Optional name value useful for styling and `menuVisibilityChange` action,
* ends up in `data-name` attribut.e
* Name useful for styling and `menuVisibilityChange` action, it ends up in
* the `data-name` attribute.
*/
name: string;
'data-cy'?: string;
Expand All @@ -41,7 +41,7 @@ interface KoboDropdownState {
}

interface AdditionalWrapperAttributes {
'data-name'?: string;
'data-name': string;
'data-cy'?: string;
}

Expand Down Expand Up @@ -229,12 +229,10 @@ export default class KoboDropdown extends React.Component<
}

render() {
const additionalWrapperAttributes: AdditionalWrapperAttributes = {};

if (this.props.name) {
const additionalWrapperAttributes: AdditionalWrapperAttributes = {
// We use `data-name` attribute to allow any character in the name.
additionalWrapperAttributes['data-name'] = this.props.name;
}
['data-name']: this.props.name,
};

if (this.props['data-cy']) {
additionalWrapperAttributes['data-cy'] = this.props['data-cy'];
Expand Down
33 changes: 15 additions & 18 deletions jsapp/js/components/common/koboSelect.scss
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,23 @@ $k-select-menu-padding: sizes.$x6;

.k-select__option {
@include mixins.buttonReset;
font-weight: 400;
position: relative;

&:hover,
&.k-select__option--selected {
color: colors.$kobo-gray-24;
background-color: colors.$kobo-gray-96;
}

&:focus-visible {
@include mixins.default-ui-focus;
// Needed so that option--selected never appears above focus styles
z-index: 1;
}

.k-icon {
color: colors.$kobo-gray-85;
color: inherit;
}
}

Expand Down Expand Up @@ -135,23 +149,6 @@ $k-select-menu-padding: sizes.$x6;
}
}

.k-select__option {
font-weight: 500;
position: relative;

&:hover,
&.k-select__option--selected {
color: colors.$kobo-gray-24;
background-color: colors.$kobo-gray-96;
}

&:focus-visible {
@include mixins.default-ui-focus;
// Needed so that option--selected never appears above focus styles
z-index: 1;
}
}

.k-select__option label,
.k-select__trigger label {
@include mixins.textEllipsis;
Expand Down
1 change: 1 addition & 0 deletions jsapp/js/components/common/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface TooltipProps {
ariaLabel: string;
/** Position of the tooltip (centered as default) */
alignment?: TooltipAlignment;
children?: React.ReactNode;
}

/**
Expand Down
9 changes: 5 additions & 4 deletions jsapp/js/components/modals/koboModal.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React from 'react';
import Modal from 'react-modal';
import cx from 'classnames';
import bem, {makeBem} from 'js/bem';
import './koboModal.scss';

bem.KoboModal = makeBem(null, 'kobo-modal');

type KoboModalSize = 'large' | 'medium';

const DEFAULT_SIZE: KoboModalSize = 'medium';

interface KoboModalProps {
/** For displaying the modal. */
isOpen: boolean;
Expand All @@ -23,16 +26,14 @@ interface KoboModalProps {
}

export default function KoboModal(props: KoboModalProps) {
const modalSize: KoboModalSize = props.size || 'medium';

const modalClassNames = ['kobo-modal', `kobo-modal--size-${modalSize}`];
const modalSize: KoboModalSize = props.size || DEFAULT_SIZE;

return (
<Modal
ariaHideApp
isOpen={props.isOpen}
onRequestClose={props.onRequestClose}
className={modalClassNames.join(' ')}
className={cx('kobo-modal', `kobo-modal--size-${modalSize}`)}
overlayClassName='kobo-modal-overlay'
shouldCloseOnOverlayClick={props.isDismissableByDefaultMeans}
shouldCloseOnEsc={props.isDismissableByDefaultMeans}
Expand Down
Loading

0 comments on commit ad38cb7

Please sign in to comment.