Skip to content

Commit

Permalink
Merge branch 'main' into leszek/task-985-member-mutation-api
Browse files Browse the repository at this point in the history
# Conflicts:
#	jsapp/js/account/organization/MembersRoute.tsx
#	jsapp/js/account/organization/membersQuery.ts
#	jsapp/js/api.endpoints.ts
#	kobo/apps/organizations/serializers.py
#	kobo/apps/organizations/views.py
  • Loading branch information
magicznyleszek committed Nov 25, 2024
2 parents 500df96 + 1386a15 commit 3847958
Show file tree
Hide file tree
Showing 23 changed files with 129 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
} from 'js/account/stripe.types';
import {isAddonProduct} from 'js/account/stripe.utils';
import styles from './addOnList.module.scss';
import {OneTimeAddOnRow} from 'js/account/add-ons/oneTimeAddOnRow.component';
import {OneTimeAddOnRow} from 'jsapp/js/account/addOns/oneTimeAddOnRow.component';
import type {BadgeColor} from 'jsapp/js/components/common/badge';
import Badge from 'jsapp/js/components/common/badge';
import {formatDate} from 'js/utils';
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import styles from 'js/account/add-ons/addOnList.module.scss';
import styles from 'js/account/addOns/addOnList.module.scss';
import React, {useMemo, useState} from 'react';
import type {
Product,
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/account/organization/MembersRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function MembersRoute() {
queryHook={useOrganizationMembersQuery}
columns={[
{
key: 'user__username',
key: 'user__extra_details__name',
label: t('Name'),
cellFormatter: (member: OrganizationMember) => (
<Avatar
Expand All @@ -46,7 +46,7 @@ export default function MembersRoute() {
isUsernameVisible
email={member.user__email}
// We pass `undefined` for the case it's an empty string
fullName={member.user__name || undefined}
fullName={member.user__extra_details__name || undefined}
/>
),
size: 360,
Expand Down
6 changes: 3 additions & 3 deletions jsapp/js/account/organization/membersQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ export interface OrganizationMember {
/** `/api/v2/users/<username>/` */
user: string;
user__username: string;
/** can be empty an string in some edge cases */
/** can be an empty string in some edge cases */
user__email: string | '';
/** can be empty an string in some edge cases */
user__name: string | '';
/** can be an empty string in some edge cases */
user__extra_details__name: string | '';
role: OrganizationUserRole;
user__has_mfa_enabled: boolean;
user__is_active: boolean;
Expand Down
12 changes: 4 additions & 8 deletions jsapp/js/account/organization/organization.utils.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {SubscriptionInfo} from 'jsapp/js/account/stripe.types';
import type {EnvStoreData} from 'jsapp/js/envStore';

/** Only use this directly for complex cases/strings (for example, possessive case).
/** Only use this directly for complex cases/strings (for example, possessive case).
* Otherwise, use getSimpleMMOLabel.
* @param {EnvStoreData} envStoreData
* @param {SubscriptionInfo} subscription
Expand All @@ -11,13 +11,9 @@ export function shouldUseTeamLabel(
envStoreData: EnvStoreData,
subscription: SubscriptionInfo | null
) {
if (subscription) {
return (
subscription.items[0].price.product.metadata?.use_team_label === 'true'
);
}

return envStoreData.use_team_label;
return subscription
? subscription.items[0].price.product.metadata?.use_team_label === 'true'
: envStoreData.use_team_label;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/account/plans/plan.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {ACTIVE_STRIPE_STATUSES} from 'js/constants';
import type {FreeTierThresholds} from 'js/envStore';
import envStore from 'js/envStore';
import useWhen from 'js/hooks/useWhen.hook';
import AddOnList from 'js/account/add-ons/addOnList.component';
import AddOnList from 'jsapp/js/account/addOns/addOnList.component';
import subscriptionStore from 'js/account/subscriptionStore';
import {when} from 'mobx';
import {
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/account/routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const PlansRoute = React.lazy(
() => import(/* webpackPrefetch: true */ './plans/plan.component')
);
export const AddOnsRoute = React.lazy(
() => import(/* webpackPrefetch: true */ './add-ons/addOns.component')
() => import(/* webpackPrefetch: true */ './addOns/addOns.component')
);
export const AccountSettings = React.lazy(
() => import(/* webpackPrefetch: true */ './accountSettingsRoute')
Expand Down
159 changes: 85 additions & 74 deletions jsapp/js/account/security/email/emailSection.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
deleteUnverifiedUserEmails,
} from './emailSection.api';
import type {EmailResponse} from './emailSection.api';
import {useOrganizationQuery} from '../../organization/organizationQuery';

// Partial components
import Button from 'jsapp/js/components/common/button';
Expand All @@ -33,6 +34,8 @@ interface EmailState {
export default function EmailSection() {
const [session] = useState(() => sessionStore);

const orgQuery = useOrganizationQuery();

let initialEmail = '';
if ('email' in session.currentAccount) {
initialEmail = session.currentAccount.email;
Expand Down Expand Up @@ -116,90 +119,98 @@ export default function EmailSection() {
const unverifiedEmail = email.emails.find(
(userEmail) => !userEmail.verified && !userEmail.primary
);
const isReady = session.isInitialLoadComplete && 'email' in currentAccount;
const userCanChangeEmail = orgQuery.data?.is_mmo
? orgQuery.data.request_user_role !== 'member'
: true;

return (
<section className={securityStyles.securitySection}>
<div className={securityStyles.securitySectionTitle}>
<h2 className={securityStyles.securitySectionTitleText}>{t('Email address')}</h2>
</div>

<div className={cx(securityStyles.securitySectionBody, styles.body)}>
{!session.isPending &&
session.isInitialLoadComplete &&
'email' in currentAccount && (
<TextBox
value={email.newEmail}
placeholder={t('Type new email address')}
onChange={onTextFieldChange.bind(onTextFieldChange)}
type='email'
/>
)}

{unverifiedEmail?.email &&
!session.isPending &&
session.isInitialLoadComplete &&
'email' in currentAccount && (
<>
<div className={styles.unverifiedEmail}>
<Icon name='alert' />
<p className={styles.blurb}>
<strong>
{t('Check your email ##UNVERIFIED_EMAIL##. ').replace(
'##UNVERIFIED_EMAIL##',
unverifiedEmail.email
)}
</strong>

{t(
'A verification link has been sent to confirm your ownership. Once confirmed, this address will replace ##UNVERIFIED_EMAIL##'
).replace('##UNVERIFIED_EMAIL##', currentAccount.email)}
</p>
</div>

<div className={styles.unverifiedEmailButtons}>
<Button
label='Resend'
size='m'
type='secondary'
onClick={resendNewUserEmail.bind(
resendNewUserEmail,
<div
className={cx([
securityStyles.securitySectionBody,
userCanChangeEmail ? styles.body : styles.emailUpdateDisabled,
])}
>
{isReady && userCanChangeEmail ? (
<TextBox
value={email.newEmail}
placeholder={t('Type new email address')}
onChange={onTextFieldChange.bind(onTextFieldChange)}
type='email'
/>
) : (
<div className={styles.emailText}>{email.newEmail}</div>
)}

{unverifiedEmail?.email && isReady && (
<>
<div className={styles.unverifiedEmail}>
<Icon name='alert' />
<p className={styles.blurb}>
<strong>
{t('Check your email ##UNVERIFIED_EMAIL##. ').replace(
'##UNVERIFIED_EMAIL##',
unverifiedEmail.email
)}
/>
<Button
label='Remove'
size='m'
type='secondary-danger'
onClick={deleteNewUserEmail}
/>
</div>

{email.refreshedEmail && (
<label>
{t('Email was sent again: ##TIMESTAMP##').replace(
'##TIMESTAMP##',
email.refreshedEmailDate
)}
</label>
)}
</>
)}
</strong>

{t(
'A verification link has been sent to confirm your ownership. Once confirmed, this address will replace ##UNVERIFIED_EMAIL##'
).replace('##UNVERIFIED_EMAIL##', currentAccount.email)}
</p>
</div>

<div className={styles.unverifiedEmailButtons}>
<Button
label='Resend'
size='m'
type='secondary'
onClick={resendNewUserEmail.bind(
resendNewUserEmail,
unverifiedEmail.email
)}
/>
<Button
label='Remove'
size='m'
type='secondary-danger'
onClick={deleteNewUserEmail}
/>
</div>

{email.refreshedEmail && (
<label>
{t('Email was sent again: ##TIMESTAMP##').replace(
'##TIMESTAMP##',
email.refreshedEmailDate
)}
</label>
)}
</>
)}
</div>

<form
className={styles.options}
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<Button
label='Change'
size='m'
type='primary'
onClick={handleSubmit}
/>
</form>
{userCanChangeEmail && (
<div className={styles.options}>
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<Button
label='Change'
size='m'
type='primary'
onClick={handleSubmit}
/>
</form>
</div>
)}
</section>
);
}
10 changes: 10 additions & 0 deletions jsapp/js/account/security/email/emailSection.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@
display: flex;
gap: 10px;
}

.emailText {
font-weight: 600;
}

.emailUpdateDisabled {
flex: 5;
// To compensate for the `options` class not displaying when there is no email
margin-right: calc(30% + 8px);
}
11 changes: 5 additions & 6 deletions jsapp/js/account/subscriptionStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {makeAutoObservable} from 'mobx';
import {handleApiFail} from 'js/api';
import {handleApiFail, fetchGet} from 'js/api';
import {ACTIVE_STRIPE_STATUSES, ROOT_URL} from 'js/constants';
import {fetchGet} from 'jsapp/js/api';
import type {PaginatedResponse} from 'js/dataInterface';
import {Product, SubscriptionInfo} from 'js/account/stripe.types';
import type {Product, SubscriptionInfo} from 'js/account/stripe.types';

const PRODUCTS_URL = '/api/v2/stripe/products/';

Expand Down Expand Up @@ -53,16 +52,16 @@ class SubscriptionStore {
);
this.canceledPlans = response.results.filter(
(sub) =>
sub.items[0]?.price.product.metadata?.product_type == 'plan' &&
sub.items[0]?.price.product.metadata?.product_type === 'plan' &&
sub.status === 'canceled'
);
// get any active plan subscriptions for the user
this.planResponse = this.activeSubscriptions.filter(
(sub) => sub.items[0]?.price.product.metadata?.product_type == 'plan'
(sub) => sub.items[0]?.price.product.metadata?.product_type === 'plan'
);
// get any active recurring add-on subscriptions for the user
this.addOnsResponse = this.activeSubscriptions.filter(
(sub) => sub.items[0]?.price.product.metadata?.product_type == 'addon'
(sub) => sub.items[0]?.price.product.metadata?.product_type === 'addon'
);

this.isPending = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import KoboModalHeader from 'js/components/modals/koboModalHeader';
import styles from './oneTimeAddOnUsageModal.module.scss';
import {OneTimeAddOn, RecurringInterval, USAGE_TYPE} from '../../stripe.types';
import {useLimitDisplay} from '../../stripe.utils';
import OneTimeAddOnList from './one-time-add-on-list/oneTimeAddOnList.component';
import OneTimeAddOnList from './oneTimeAddOnList/oneTimeAddOnList.component';

interface OneTimeAddOnUsageModalProps {
type: USAGE_TYPE;
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/account/usage/usageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import cx from 'classnames';
import subscriptionStore from 'js/account/subscriptionStore';
import Badge from 'js/components/common/badge';
import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
import OneTimeAddOnUsageModal from './one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component';
import OneTimeAddOnUsageModal from './oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component';

interface UsageContainerProps {
usage: number;
Expand Down
6 changes: 4 additions & 2 deletions kobo/apps/organizations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ class OrganizationUserSerializer(serializers.ModelSerializer):
source='user.date_joined', format='%Y-%m-%dT%H:%M:%SZ'
)
user__username = serializers.ReadOnlyField(source='user.username')
user__name = serializers.ReadOnlyField(source='user.get_full_name')
user__extra_details__name = serializers.ReadOnlyField(
source='user.extra_details.data.name'
)
user__email = serializers.ReadOnlyField(source='user.email')
user__is_active = serializers.ReadOnlyField(source='user.is_active')

Expand All @@ -40,7 +42,7 @@ class Meta:
'user',
'user__username',
'user__email',
'user__name',
'user__extra_details__name',
'role',
'user__has_mfa_enabled',
'date_joined',
Expand Down
7 changes: 6 additions & 1 deletion kobo/apps/organizations/tests/test_organizations_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ def test_anonymous_user(self):
def test_create(self):
data = {'name': 'my org'}
res = self.client.post(self.url_list, data)
self.assertContains(res, data['name'], status_code=201)
self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

def test_delete(self):
self._insert_data()
res = self.client.delete(self.url_detail)
self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

def test_list(self):
self._insert_data()
Expand Down
Loading

0 comments on commit 3847958

Please sign in to comment.