Change prettier rules

This commit is contained in:
ErickSkrauch
2020-05-24 02:08:24 +03:00
parent 73f0c37a6a
commit f85b9d8d35
382 changed files with 24137 additions and 26046 deletions

View File

@@ -1,26 +1,26 @@
{
"accountsForDevelopers": "Ely.by Accounts for developers",
"accountsAllowsYouYoUseOauth2": "Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}.",
"ourDocumentation": "our documentation",
"ifYouHaveAnyTroubles": "If you are experiencing difficulties, you can always use {feedback}. We'll surely help you.",
"feedback": "feedback",
"weDontKnowAnythingAboutYou": "We don't know anything about you yet.",
"youMustAuthToBegin": "You have to authorize to start.",
"authorization": "Authorization",
"youDontHaveAnyApplication": "You don't have any app registered yet.",
"shallWeStart": "Shall we start?",
"addNew": "Add new",
"yourApplications": "Your applications:",
"countUsers": "{count, plural, =0 {No users} one {# user} other {# users}}",
"ifYouSuspectingThatSecretHasBeenCompromised": "If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It'll cause recall of the all \"access\" and \"refresh\" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.",
"revokeAllTokens": "Revoke all tokens",
"resetClientSecret": "Reset Client Secret",
"delete": "Delete",
"editDescription": "{icon} Edit description",
"allRefreshTokensWillBecomeInvalid": "All \"refresh\" tokens will become invalid and after next authorization the user will get permissions prompt.",
"appAndAllTokenWillBeDeleted": "Application and all associated tokens will be deleted.",
"takeCareAccessTokensInvalidation": "Take care because \"access\" tokens won't be invalidated immediately.",
"cancel": "Cancel",
"continue": "Continue",
"performing": "Performing…"
"accountsForDevelopers": "Ely.by Accounts for developers",
"accountsAllowsYouYoUseOauth2": "Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}.",
"ourDocumentation": "our documentation",
"ifYouHaveAnyTroubles": "If you are experiencing difficulties, you can always use {feedback}. We'll surely help you.",
"feedback": "feedback",
"weDontKnowAnythingAboutYou": "We don't know anything about you yet.",
"youMustAuthToBegin": "You have to authorize to start.",
"authorization": "Authorization",
"youDontHaveAnyApplication": "You don't have any app registered yet.",
"shallWeStart": "Shall we start?",
"addNew": "Add new",
"yourApplications": "Your applications:",
"countUsers": "{count, plural, =0 {No users} one {# user} other {# users}}",
"ifYouSuspectingThatSecretHasBeenCompromised": "If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It'll cause recall of the all \"access\" and \"refresh\" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.",
"revokeAllTokens": "Revoke all tokens",
"resetClientSecret": "Reset Client Secret",
"delete": "Delete",
"editDescription": "{icon} Edit description",
"allRefreshTokensWillBecomeInvalid": "All \"refresh\" tokens will become invalid and after next authorization the user will get permissions prompt.",
"appAndAllTokenWillBeDeleted": "Application and all associated tokens will be deleted.",
"takeCareAccessTokensInvalidation": "Take care because \"access\" tokens won't be invalidated immediately.",
"cancel": "Cancel",
"continue": "Continue",
"performing": "Performing…"
}

View File

@@ -15,144 +15,133 @@ import toolsIcon from './icons/tools.svg';
import ApplicationsList from './list';
type Props = {
clientId: string | null;
resetClientId: () => void; // notify parent to remove clientId from current location.href
displayForGuest: boolean;
applications: Array<OauthAppResponse>;
isLoading: boolean;
deleteApp: (clientId: string) => Promise<any>;
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
clientId: string | null;
resetClientId: () => void; // notify parent to remove clientId from current location.href
displayForGuest: boolean;
applications: Array<OauthAppResponse>;
isLoading: boolean;
deleteApp: (clientId: string) => Promise<any>;
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
};
export default class ApplicationsIndex extends React.Component<Props> {
render() {
return (
<div className={styles.container}>
<div className={styles.welcomeContainer}>
<Message {...messages.accountsForDevelopers}>
{(pageTitle: string) => (
<h2 className={styles.welcomeTitle}>
<Helmet title={pageTitle} />
{pageTitle}
</h2>
)}
</Message>
<div className={styles.welcomeTitleDelimiter} />
<div className={styles.welcomeParagraph}>
<Message
{...messages.accountsAllowsYouYoUseOauth2}
values={{
ourDocumentation: (
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
<Message {...messages.ourDocumentation} />
</a>
),
}}
/>
</div>
<div className={styles.welcomeParagraph}>
<Message
{...messages.ifYouHaveAnyTroubles}
values={{
feedback: (
<ContactLink>
<Message {...messages.feedback} />
</ContactLink>
),
}}
/>
</div>
</div>
render() {
return (
<div className={styles.container}>
<div className={styles.welcomeContainer}>
<Message {...messages.accountsForDevelopers}>
{(pageTitle: string) => (
<h2 className={styles.welcomeTitle}>
<Helmet title={pageTitle} />
{pageTitle}
</h2>
)}
</Message>
<div className={styles.welcomeTitleDelimiter} />
<div className={styles.welcomeParagraph}>
<Message
{...messages.accountsAllowsYouYoUseOauth2}
values={{
ourDocumentation: (
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
<Message {...messages.ourDocumentation} />
</a>
),
}}
/>
</div>
<div className={styles.welcomeParagraph}>
<Message
{...messages.ifYouHaveAnyTroubles}
values={{
feedback: (
<ContactLink>
<Message {...messages.feedback} />
</ContactLink>
),
}}
/>
</div>
</div>
{this.getContent()}
</div>
);
}
getContent() {
const {
displayForGuest,
applications,
isLoading,
resetApp,
deleteApp,
clientId,
resetClientId,
} = this.props;
if (applications.length > 0) {
return (
<ApplicationsList
applications={applications}
resetApp={resetApp}
deleteApp={deleteApp}
clientId={clientId}
resetClientId={resetClientId}
/>
);
{this.getContent()}
</div>
);
}
if (displayForGuest) {
return <Guest />;
}
getContent() {
const { displayForGuest, applications, isLoading, resetApp, deleteApp, clientId, resetClientId } = this.props;
return <Loader noApps={!isLoading} />;
}
if (applications.length > 0) {
return (
<ApplicationsList
applications={applications}
resetApp={resetApp}
deleteApp={deleteApp}
clientId={clientId}
resetClientId={resetClientId}
/>
);
}
if (displayForGuest) {
return <Guest />;
}
return <Loader noApps={!isLoading} />;
}
}
function Loader({ noApps }: { noApps: boolean }) {
return (
<div className={styles.emptyState} data-e2e={noApps ? 'noApps' : 'loading'}>
<img
src={noApps ? cubeIcon : loadingCubeIcon}
className={styles.emptyStateIcon}
/>
return (
<div className={styles.emptyState} data-e2e={noApps ? 'noApps' : 'loading'}>
<img src={noApps ? cubeIcon : loadingCubeIcon} className={styles.emptyStateIcon} />
<div
className={clsx(styles.noAppsContainer, {
[styles.noAppsAnimating]: noApps,
})}
>
<div className={styles.emptyStateText}>
<div>
<Message {...messages.youDontHaveAnyApplication} />
</div>
<div>
<Message {...messages.shallWeStart} />
</div>
<div
className={clsx(styles.noAppsContainer, {
[styles.noAppsAnimating]: noApps,
})}
>
<div className={styles.emptyStateText}>
<div>
<Message {...messages.youDontHaveAnyApplication} />
</div>
<div>
<Message {...messages.shallWeStart} />
</div>
</div>
<LinkButton
to="/dev/applications/new"
data-e2e="newApp"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.emptyStateActionButton}
/>
</div>
</div>
<LinkButton
to="/dev/applications/new"
data-e2e="newApp"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.emptyStateActionButton}
/>
</div>
</div>
);
);
}
function Guest() {
return (
<div className={styles.emptyState}>
<img src={toolsIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}>
<div>
<Message {...messages.weDontKnowAnythingAboutYou} />
</div>
<div>
<Message {...messages.youMustAuthToBegin} />
</div>
</div>
return (
<div className={styles.emptyState}>
<img src={toolsIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}>
<div>
<Message {...messages.weDontKnowAnythingAboutYou} />
</div>
<div>
<Message {...messages.youMustAuthToBegin} />
</div>
</div>
<LinkButton
to="/login"
label={messages.authorization}
color={COLOR_BLUE}
className={styles.emptyStateActionButton}
/>
</div>
);
<LinkButton
to="/login"
label={messages.authorization}
color={COLOR_BLUE}
className={styles.emptyStateActionButton}
/>
</div>
);
}

View File

@@ -7,94 +7,85 @@ import { ThunkAction } from 'app/reducers';
import { Apps } from './reducer';
interface SetAvailableAction extends ReduxAction {
type: 'apps:setAvailable';
payload: Array<OauthAppResponse>;
type: 'apps:setAvailable';
payload: Array<OauthAppResponse>;
}
export function setAppsList(apps: Array<OauthAppResponse>): SetAvailableAction {
return {
type: 'apps:setAvailable',
payload: apps,
};
return {
type: 'apps:setAvailable',
payload: apps,
};
}
export function getApp(
state: { apps: Apps },
clientId: string,
): OauthAppResponse | null {
return state.apps.available.find((app) => app.clientId === clientId) || null;
export function getApp(state: { apps: Apps }, clientId: string): OauthAppResponse | null {
return state.apps.available.find((app) => app.clientId === clientId) || null;
}
export function fetchApp(clientId: string): ThunkAction<Promise<void>> {
return async (dispatch) => {
const app = await oauth.getApp(clientId);
return async (dispatch) => {
const app = await oauth.getApp(clientId);
dispatch(addApp(app));
};
dispatch(addApp(app));
};
}
interface AddAppAction extends ReduxAction {
type: 'apps:addApp';
payload: OauthAppResponse;
type: 'apps:addApp';
payload: OauthAppResponse;
}
function addApp(app: OauthAppResponse): AddAppAction {
return {
type: 'apps:addApp',
payload: app,
};
return {
type: 'apps:addApp',
payload: app,
};
}
export function fetchAvailableApps() {
return async (
dispatch: Dispatch<any>,
getState: () => { user: User },
): Promise<void> => {
const { id } = getState().user;
return async (dispatch: Dispatch<any>, getState: () => { user: User }): Promise<void> => {
const { id } = getState().user;
if (!id) {
dispatch(setAppsList([]));
if (!id) {
dispatch(setAppsList([]));
return;
}
return;
}
const apps = await oauth.getAppsByUser(id);
const apps = await oauth.getAppsByUser(id);
dispatch(setAppsList(apps));
};
dispatch(setAppsList(apps));
};
}
export function deleteApp(clientId: string) {
return async (dispatch: Dispatch<any>): Promise<void> => {
await oauth.delete(clientId);
return async (dispatch: Dispatch<any>): Promise<void> => {
await oauth.delete(clientId);
dispatch(createDeleteAppAction(clientId));
};
dispatch(createDeleteAppAction(clientId));
};
}
interface DeleteAppAction extends ReduxAction {
type: 'apps:deleteApp';
payload: string;
type: 'apps:deleteApp';
payload: string;
}
function createDeleteAppAction(clientId: string): DeleteAppAction {
return {
type: 'apps:deleteApp',
payload: clientId,
};
return {
type: 'apps:deleteApp',
payload: clientId,
};
}
export function resetApp(
clientId: string,
resetSecret: boolean,
): ThunkAction<Promise<void>> {
return async (dispatch) => {
const { data: app } = await oauth.reset(clientId, resetSecret);
export function resetApp(clientId: string, resetSecret: boolean): ThunkAction<Promise<void>> {
return async (dispatch) => {
const { data: app } = await oauth.reset(clientId, resetSecret);
if (resetSecret) {
dispatch(addApp(app));
}
};
if (resetSecret) {
dispatch(addApp(app));
}
};
}
export type Action = SetAvailableAction | DeleteAppAction | AddAppAction;

View File

@@ -1,20 +1,20 @@
{
"creatingApplication": "Creating an application",
"website": "Web site",
"minecraftServer": "Minecraft server",
"toDisplayRegistrationFormChooseType": "To display registration form for a new application choose necessary type.",
"applicationName": "Application name:",
"appDescriptionWillBeAlsoVisibleOnOauthPage": "Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
"description": "Description:",
"websiteLinkWillBeUsedAsAdditionalId": "Site's link is optional, but it can be used as an additional identifier of the application.",
"websiteLink": "Website link:",
"redirectUriLimitsAllowableBaseAddress": "Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
"redirectUri": "Redirect URI:",
"createApplication": "Create application",
"serverName": "Server name:",
"ipAddressIsOptionButPreferable": "IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=",
"serverIp": "Server IP:",
"youCanAlsoSpecifyServerSite": "You also can specify either server's site URL or its community in a social network.",
"updatingApplication": "Updating an application",
"updateApplication": "Update application"
"creatingApplication": "Creating an application",
"website": "Web site",
"minecraftServer": "Minecraft server",
"toDisplayRegistrationFormChooseType": "To display registration form for a new application choose necessary type.",
"applicationName": "Application name:",
"appDescriptionWillBeAlsoVisibleOnOauthPage": "Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
"description": "Description:",
"websiteLinkWillBeUsedAsAdditionalId": "Site's link is optional, but it can be used as an additional identifier of the application.",
"websiteLink": "Website link:",
"redirectUriLimitsAllowableBaseAddress": "Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
"redirectUri": "Redirect URI:",
"createApplication": "Create application",
"serverName": "Server name:",
"ipAddressIsOptionButPreferable": "IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=",
"serverIp": "Server IP:",
"youCanAlsoSpecifyServerSite": "You also can specify either server's site URL or its community in a social network.",
"updatingApplication": "Updating an application",
"updateApplication": "Update application"
}

View File

@@ -7,10 +7,7 @@ import { ApplicationType } from 'app/components/dev/apps';
import { Form, FormModel, Button } from 'app/components/ui/form';
import { BackButton } from 'app/components/profile/ProfileForm';
import { COLOR_GREEN } from 'app/components/ui';
import {
TYPE_APPLICATION,
TYPE_MINECRAFT_SERVER,
} from 'app/components/dev/apps';
import { TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'app/components/dev/apps';
import styles from 'app/components/profile/profileForm.scss';
import logger from 'app/services/logger';
import messages from './ApplicationForm.intl.json';
@@ -20,125 +17,116 @@ import WebsiteType from './WebsiteType';
import MinecraftServerType from './MinecraftServerType';
type TypeToForm = Record<
ApplicationType,
{
label: MessageDescriptor;
component: React.ComponentType<any>;
}
ApplicationType,
{
label: MessageDescriptor;
component: React.ComponentType<any>;
}
>;
const typeToForm: TypeToForm = {
[TYPE_APPLICATION]: {
label: messages.website,
component: WebsiteType,
},
[TYPE_MINECRAFT_SERVER]: {
label: messages.minecraftServer,
component: MinecraftServerType,
},
[TYPE_APPLICATION]: {
label: messages.website,
component: WebsiteType,
},
[TYPE_MINECRAFT_SERVER]: {
label: messages.minecraftServer,
component: MinecraftServerType,
},
};
type TypeToLabel = Record<ApplicationType, MessageDescriptor>;
const typeToLabel: TypeToLabel = ((Object.keys(typeToForm) as unknown) as Array<
ApplicationType
>).reduce((result, key) => {
result[key] = typeToForm[key].label;
const typeToLabel: TypeToLabel = ((Object.keys(typeToForm) as unknown) as Array<ApplicationType>).reduce(
(result, key) => {
result[key] = typeToForm[key].label;
return result;
}, {} as TypeToLabel);
return result;
},
{} as TypeToLabel,
);
export default class ApplicationForm extends React.Component<{
app: OauthAppResponse;
form: FormModel;
displayTypeSwitcher?: boolean;
type: ApplicationType | null;
setType: (type: ApplicationType) => void;
onSubmit: (form: FormModel) => Promise<void>;
app: OauthAppResponse;
form: FormModel;
displayTypeSwitcher?: boolean;
type: ApplicationType | null;
setType: (type: ApplicationType) => void;
onSubmit: (form: FormModel) => Promise<void>;
}> {
static defaultProps = {
setType: () => {},
};
static defaultProps = {
setType: () => {},
};
render() {
const { type, setType, form, displayTypeSwitcher, app } = this.props;
const { component: FormComponent } = (type && typeToForm[type]) || {};
const isUpdate = app.clientId !== '';
render() {
const { type, setType, form, displayTypeSwitcher, app } = this.props;
const { component: FormComponent } = (type && typeToForm[type]) || {};
const isUpdate = app.clientId !== '';
return (
<Form form={form} onSubmit={this.onFormSubmit}>
<div className={styles.contentWithBackButton}>
<BackButton to="/dev/applications" />
return (
<Form form={form} onSubmit={this.onFormSubmit}>
<div className={styles.contentWithBackButton}>
<BackButton to="/dev/applications" />
<div className={styles.form}>
<div className={styles.formBody}>
<Message
{...(isUpdate
? messages.updatingApplication
: messages.creatingApplication)}
>
{(pageTitle: string) => (
<h3 className={styles.title}>
<Helmet title={pageTitle} />
{pageTitle}
</h3>
)}
</Message>
<div className={styles.form}>
<div className={styles.formBody}>
<Message {...(isUpdate ? messages.updatingApplication : messages.creatingApplication)}>
{(pageTitle: string) => (
<h3 className={styles.title}>
<Helmet title={pageTitle} />
{pageTitle}
</h3>
)}
</Message>
{displayTypeSwitcher && (
<div className={styles.formRow}>
<ApplicationTypeSwitcher
selectedType={type}
setType={setType}
appTypes={typeToLabel}
/>
{displayTypeSwitcher && (
<div className={styles.formRow}>
<ApplicationTypeSwitcher
selectedType={type}
setType={setType}
appTypes={typeToLabel}
/>
</div>
)}
{FormComponent ? (
<FormComponent form={form} app={app} />
) : (
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.toDisplayRegistrationFormChooseType} />
</p>
</div>
)}
</div>
</div>
{!!FormComponent && (
<Button
color={COLOR_GREEN}
block
label={isUpdate ? messages.updateApplication : messages.createApplication}
type="submit"
/>
)}
</div>
)}
{FormComponent ? (
<FormComponent form={form} app={app} />
) : (
<div className={styles.formRow}>
<p className={styles.description}>
<Message
{...messages.toDisplayRegistrationFormChooseType}
/>
</p>
</div>
)}
</div>
</div>
{!!FormComponent && (
<Button
color={COLOR_GREEN}
block
label={
isUpdate
? messages.updateApplication
: messages.createApplication
}
type="submit"
/>
)}
</div>
</Form>
);
}
onFormSubmit = async () => {
const { form } = this.props;
try {
await this.props.onSubmit(form);
} catch (resp) {
if (resp.errors) {
form.setErrors(resp.errors);
return;
}
logger.unexpected(new Error('Error submitting application form'), resp);
</Form>
);
}
};
onFormSubmit = async () => {
const { form } = this.props;
try {
await this.props.onSubmit(form);
} catch (resp) {
if (resp.errors) {
form.setErrors(resp.errors);
return;
}
logger.unexpected(new Error('Error submitting application form'), resp);
}
};
}

View File

@@ -7,31 +7,25 @@ import { Radio } from 'app/components/ui/form';
import styles from './applicationTypeSwitcher.scss';
interface Props {
appTypes: Record<ApplicationType, MessageDescriptor>;
selectedType: ApplicationType | null;
setType: (type: ApplicationType) => void;
appTypes: Record<ApplicationType, MessageDescriptor>;
selectedType: ApplicationType | null;
setType: (type: ApplicationType) => void;
}
const ApplicationTypeSwitcher: ComponentType<Props> = ({
appTypes,
selectedType,
setType,
}) => (
<div>
{((Object.keys(appTypes) as unknown) as Array<ApplicationType>).map(
(type) => (
<div className={styles.radioContainer} key={type}>
<Radio
onChange={() => setType(type)}
skin={SKIN_LIGHT}
label={appTypes[type]}
value={type}
checked={selectedType === type}
/>
</div>
),
)}
</div>
const ApplicationTypeSwitcher: ComponentType<Props> = ({ appTypes, selectedType, setType }) => (
<div>
{((Object.keys(appTypes) as unknown) as Array<ApplicationType>).map((type) => (
<div className={styles.radioContainer} key={type}>
<Radio
onChange={() => setType(type)}
skin={SKIN_LIGHT}
label={appTypes[type]}
value={type}
checked={selectedType === type}
/>
</div>
))}
</div>
);
export default ApplicationTypeSwitcher;

View File

@@ -8,50 +8,50 @@ import styles from 'app/components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
interface Props {
form: FormModel;
app: OauthAppResponse;
form: FormModel;
app: OauthAppResponse;
}
const MinecraftServerType: ComponentType<Props> = ({ form, app }) => (
<div>
<div className={styles.formRow}>
<Input
{...form.bindField('name')}
label={messages.serverName}
defaultValue={app.name}
required
skin={SKIN_LIGHT}
/>
</div>
<div>
<div className={styles.formRow}>
<Input
{...form.bindField('name')}
label={messages.serverName}
defaultValue={app.name}
required
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.ipAddressIsOptionButPreferable} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('minecraftServerIp')}
label={messages.serverIp}
defaultValue={app.minecraftServerIp}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.ipAddressIsOptionButPreferable} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('minecraftServerIp')}
label={messages.serverIp}
defaultValue={app.minecraftServerIp}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.youCanAlsoSpecifyServerSite} />
</p>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.youCanAlsoSpecifyServerSite} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
</div>
);
export default MinecraftServerType;

View File

@@ -8,66 +8,66 @@ import styles from 'app/components/profile/profileForm.scss';
import messages from './ApplicationForm.intl.json';
interface Props {
form: FormModel;
app: OauthAppResponse;
form: FormModel;
app: OauthAppResponse;
}
const WebsiteType: ComponentType<Props> = ({ form, app }) => (
<div>
<div className={styles.formRow}>
<Input
{...form.bindField('name')}
label={messages.applicationName}
defaultValue={app.name}
required
skin={SKIN_LIGHT}
/>
</div>
<div>
<div className={styles.formRow}>
<Input
{...form.bindField('name')}
label={messages.applicationName}
defaultValue={app.name}
required
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
</p>
</div>
<div className={styles.formRow}>
<TextArea
{...form.bindField('description')}
label={messages.description}
defaultValue={app.description}
skin={SKIN_LIGHT}
minRows={3}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
</p>
</div>
<div className={styles.formRow}>
<TextArea
{...form.bindField('description')}
label={messages.description}
defaultValue={app.description}
skin={SKIN_LIGHT}
minRows={3}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.websiteLinkWillBeUsedAsAdditionalId} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.websiteLinkWillBeUsedAsAdditionalId} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('websiteUrl')}
label={messages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.redirectUriLimitsAllowableBaseAddress} />
</p>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.redirectUriLimitsAllowableBaseAddress} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('redirectUri')}
label={messages.redirectUri}
defaultValue={app.redirectUri}
required
skin={SKIN_LIGHT}
/>
</div>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('redirectUri')}
label={messages.redirectUri}
defaultValue={app.redirectUri}
required
skin={SKIN_LIGHT}
/>
</div>
</div>
);
export default WebsiteType;

View File

@@ -1,3 +1,3 @@
.radioContainer {
margin-top: 10px;
margin-top: 10px;
}

View File

@@ -2,261 +2,261 @@
@import '~app/components/ui/colors.scss';
.container {
max-width: 500px;
margin: 0 auto;
background: white;
border-bottom: 10px solid #ddd8ce;
max-width: 500px;
margin: 0 auto;
background: white;
border-bottom: 10px solid #ddd8ce;
@media (max-width: 540px) {
margin: 0 20px;
}
@media (max-width: 540px) {
margin: 0 20px;
}
}
.welcomeContainer {
padding: 30px;
background: #f5f5f5;
text-align: center;
border-bottom: 1px solid #eeeeee;
padding: 30px;
background: #f5f5f5;
text-align: center;
border-bottom: 1px solid #eeeeee;
}
.welcomeTitle {
font-size: 30px;
font-family: $font-family-title;
max-width: 245px;
margin: 0 auto 15px;
line-height: 1.2;
font-size: 30px;
font-family: $font-family-title;
max-width: 245px;
margin: 0 auto 15px;
line-height: 1.2;
}
.welcomeTitleDelimiter {
width: 86px;
height: 3px;
background: $green;
margin: 0 auto 15px;
width: 86px;
height: 3px;
background: $green;
margin: 0 auto 15px;
}
.welcomeParagraph {
color: #666666;
font-size: 14px;
margin-bottom: 15px;
line-height: 1.3;
color: #666666;
font-size: 14px;
margin-bottom: 15px;
line-height: 1.3;
&:last-of-type {
margin-bottom: 0;
}
&:last-of-type {
margin-bottom: 0;
}
}
.emptyState {
padding: 30px 30px 50px;
text-align: center;
padding: 30px 30px 50px;
text-align: center;
}
.emptyStateIcon {
width: 120px;
height: 120px;
margin-bottom: 20px;
width: 120px;
height: 120px;
margin-bottom: 20px;
}
@mixin emptyStateAnimation($order) {
animation: slide-in-bottom 1s // Total animation time
0.2s + 0.2s * $order // Increase each next element delay
cubic-bezier(0.075, 0.82, 0.165, 1) // easeOutCirc
both;
animation: slide-in-bottom 1s // Total animation time
0.2s + 0.2s * $order // Increase each next element delay
cubic-bezier(0.075, 0.82, 0.165, 1) // easeOutCirc
both;
}
.emptyStateText {
font-family: $font-family-title;
color: #666666;
font-size: 16px;
margin-bottom: 20px;
line-height: 20px;
font-family: $font-family-title;
color: #666666;
font-size: 16px;
margin-bottom: 20px;
line-height: 20px;
}
.noAppsContainer {
visibility: hidden;
visibility: hidden;
}
.noAppsAnimating {
visibility: visible;
visibility: visible;
.emptyStateText {
> div {
&:nth-child(1) {
@include emptyStateAnimation(0);
}
.emptyStateText {
> div {
&:nth-child(1) {
@include emptyStateAnimation(0);
}
&:nth-child(2) {
@include emptyStateAnimation(1);
}
&:nth-child(2) {
@include emptyStateAnimation(1);
}
}
}
}
.emptyStateActionButton {
@include emptyStateAnimation(2);
}
.emptyStateActionButton {
@include emptyStateAnimation(2);
}
}
@keyframes slide-in-bottom {
0% {
transform: translateY(50px);
opacity: 0;
}
0% {
transform: translateY(50px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.appsListTitleContainer {
display: flex;
align-items: center;
padding: 20px 30px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
padding: 20px 30px;
border-bottom: 1px solid #eee;
}
.appsListTitle {
font-family: $font-family-title;
font-size: 24px;
flex-grow: 1;
font-family: $font-family-title;
font-size: 24px;
flex-grow: 1;
}
.appsListAddNewAppBtn {
}
.appsListContainer {
margin-bottom: 30px;
margin-bottom: 30px;
}
.appItemContainer {
border-bottom: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.appItemTile {
padding: 15px 30px;
display: flex;
align-items: center;
cursor: pointer;
transition: background-color 0.25s;
padding: 15px 30px;
display: flex;
align-items: center;
cursor: pointer;
transition: background-color 0.25s;
}
.appTileTitle {
flex-grow: 1;
flex-grow: 1;
}
.appName {
font-family: $font-family-title;
font-size: 24px;
font-family: $font-family-title;
font-size: 24px;
}
.appStats {
color: #999;
font-size: 14px;
color: #999;
font-size: 14px;
}
.appItemToggle {
}
.appItemToggleIcon {
composes: arrowRight from '~app/components/ui/icons.scss';
composes: arrowRight from '~app/components/ui/icons.scss';
position: relative;
left: 0;
position: relative;
left: 0;
font-size: 28px;
color: #ebe8e1;
font-size: 28px;
color: #ebe8e1;
transition: 0.25s;
transition: 0.25s;
.appItemTile:hover & {
color: #777;
}
.appItemTile:hover & {
color: #777;
}
.appExpanded & {
color: #777;
transform: rotate(360deg) !important; // Prevent it from hover rotating
}
.appExpanded & {
color: #777;
transform: rotate(360deg) !important; // Prevent it from hover rotating
}
}
$appDetailsContainerRightLeftPadding: 30px;
.appDetailsContainer {
background: #f5f5f5;
border-top: 1px solid #eee;
padding: 5px $appDetailsContainerRightLeftPadding;
position: relative;
transition: transform 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
background: #f5f5f5;
border-top: 1px solid #eee;
padding: 5px $appDetailsContainerRightLeftPadding;
position: relative;
transition: transform 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.appDetailsInfoField {
position: relative;
margin-bottom: 20px;
position: relative;
margin-bottom: 20px;
}
.editAppLink {
position: absolute;
top: 4px;
right: 0;
position: absolute;
top: 4px;
right: 0;
font-size: 12px;
color: #9a9a9a;
border-bottom: 0;
font-size: 12px;
color: #9a9a9a;
border-bottom: 0;
}
.pencilIcon {
composes: pencil from '~app/components/ui/icons.scss';
composes: pencil from '~app/components/ui/icons.scss';
font-size: 14px;
position: relative;
bottom: 2px;
font-size: 14px;
position: relative;
bottom: 2px;
}
.appDetailsDescription {
font-size: 12px;
color: #9a9a9a;
line-height: 1.4;
margin-bottom: 20px;
font-size: 12px;
color: #9a9a9a;
line-height: 1.4;
margin-bottom: 20px;
}
.appActionsButtons {
}
.appActionButton {
margin: 0 10px 10px 0;
margin: 0 10px 10px 0;
&:last-of-type {
margin-right: 0;
}
&:last-of-type {
margin-right: 0;
}
}
.appActionContainer {
position: absolute;
top: 100%;
left: 0;
padding: 0 $appDetailsContainerRightLeftPadding;
background: #f5f5f5;
position: absolute;
top: 100%;
left: 0;
padding: 0 $appDetailsContainerRightLeftPadding;
background: #f5f5f5;
}
.appActionDescription {
composes: appDetailsDescription;
composes: appDetailsDescription;
margin-top: 6px;
margin-top: 6px;
}
.continueActionButtonWrapper {
display: inline-block;
margin-left: 10px;
display: inline-block;
margin-left: 10px;
}
.continueActionLink {
composes: textLink from '~app/index.scss';
composes: textLink from '~app/index.scss';
font-family: $font-family-title;
font-size: 14px;
color: #666;
font-family: $font-family-title;
font-size: 14px;
color: #666;
}
.performingAction {
font-family: $font-family-title;
font-size: 14px;
color: #666;
font-family: $font-family-title;
font-size: 14px;
color: #666;
}

View File

@@ -14,292 +14,273 @@ const ACTION_REVOKE_TOKENS = 'revoke-tokens';
const ACTION_RESET_SECRET = 'reset-secret';
const ACTION_DELETE = 'delete';
const actionButtons = [
{
type: ACTION_REVOKE_TOKENS,
label: messages.revokeAllTokens,
},
{
type: ACTION_RESET_SECRET,
label: messages.resetClientSecret,
},
{
type: ACTION_DELETE,
label: messages.delete,
},
{
type: ACTION_REVOKE_TOKENS,
label: messages.revokeAllTokens,
},
{
type: ACTION_RESET_SECRET,
label: messages.resetClientSecret,
},
{
type: ACTION_DELETE,
label: messages.delete,
},
];
interface State {
selectedAction: string | null;
isActionPerforming: boolean;
detailsHeight: number;
translateY: number;
selectedAction: string | null;
isActionPerforming: boolean;
detailsHeight: number;
translateY: number;
}
export default class ApplicationItem extends React.Component<
{
application: OauthAppResponse;
expand: boolean;
onTileClick: (clientId: string) => void;
onResetSubmit: (
clientId: string,
resetClientSecret: boolean,
) => Promise<void>;
onDeleteSubmit: (clientId: string) => Promise<void>;
},
State
{
application: OauthAppResponse;
expand: boolean;
onTileClick: (clientId: string) => void;
onResetSubmit: (clientId: string, resetClientSecret: boolean) => Promise<void>;
onDeleteSubmit: (clientId: string) => Promise<void>;
},
State
> {
state: State = {
selectedAction: null,
isActionPerforming: false,
translateY: 0,
detailsHeight: 0,
};
state: State = {
selectedAction: null,
isActionPerforming: false,
translateY: 0,
detailsHeight: 0,
};
actionContainer: HTMLDivElement | null;
actionContainer: HTMLDivElement | null;
render() {
const { application: app, expand } = this.props;
const { selectedAction, translateY } = this.state;
return (
<div
className={clsx(styles.appItemContainer, {
[styles.appExpanded]: expand,
})}
data-e2e="appItem"
data-e2e-app-name={app.name}
>
<div className={styles.appItemTile} onClick={this.onTileToggle}>
<div className={styles.appTileTitle}>
<div className={styles.appName}>{app.name}</div>
<div className={styles.appStats}>
Client ID: {app.clientId}
{typeof app.countUsers !== 'undefined' && (
<span>
{' | '}
<Message
{...messages.countUsers}
values={{
count: app.countUsers,
}}
/>
</span>
)}
</div>
</div>
<div className={styles.appItemToggle}>
<div className={styles.appItemToggleIcon} />
</div>
</div>
<Collapse isOpened={expand} onRest={this.onCollapseRest}>
<div
className={styles.appDetailsContainer}
style={{ transform: `translateY(-${translateY}px)` }}
>
<div className={styles.appDetailsInfoField}>
<Link
to={`/dev/applications/${app.clientId}`}
className={styles.editAppLink}
>
<Message
{...messages.editDescription}
values={{
icon: <div className={styles.pencilIcon} />,
}}
/>
</Link>
<Input
label="Client ID:"
skin={SKIN_LIGHT}
disabled
value={app.clientId}
copy
/>
</div>
<div className={styles.appDetailsInfoField}>
<Input
label="Client Secret:"
skin={SKIN_LIGHT}
disabled
value={app.clientSecret}
data-testid="client-secret"
copy
/>
</div>
<div className={styles.appDetailsDescription}>
<Message
{...messages.ifYouSuspectingThatSecretHasBeenCompromised}
/>
</div>
<div className={styles.appActionsButtons}>
{actionButtons.map(({ type, label }) => (
<Button
key={type}
label={label}
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={!!selectedAction && selectedAction !== type}
onClick={this.onActionButtonClick(type)}
small
/>
))}
</div>
render() {
const { application: app, expand } = this.props;
const { selectedAction, translateY } = this.state;
return (
<div
className={styles.appActionContainer}
ref={(el) => {
this.actionContainer = el;
}}
className={clsx(styles.appItemContainer, {
[styles.appExpanded]: expand,
})}
data-e2e="appItem"
data-e2e-app-name={app.name}
>
{this.getActionContent()}
</div>
</div>
</Collapse>
</div>
);
}
<div className={styles.appItemTile} onClick={this.onTileToggle}>
<div className={styles.appTileTitle}>
<div className={styles.appName}>{app.name}</div>
<div className={styles.appStats}>
Client ID: {app.clientId}
{typeof app.countUsers !== 'undefined' && (
<span>
{' | '}
<Message
{...messages.countUsers}
values={{
count: app.countUsers,
}}
/>
</span>
)}
</div>
</div>
<div className={styles.appItemToggle}>
<div className={styles.appItemToggleIcon} />
</div>
</div>
getActionContent() {
const { selectedAction, isActionPerforming } = this.state;
<Collapse isOpened={expand} onRest={this.onCollapseRest}>
<div className={styles.appDetailsContainer} style={{ transform: `translateY(-${translateY}px)` }}>
<div className={styles.appDetailsInfoField}>
<Link to={`/dev/applications/${app.clientId}`} className={styles.editAppLink}>
<Message
{...messages.editDescription}
values={{
icon: <div className={styles.pencilIcon} />,
}}
/>
</Link>
<Input label="Client ID:" skin={SKIN_LIGHT} disabled value={app.clientId} copy />
</div>
switch (selectedAction) {
case ACTION_REVOKE_TOKENS:
case ACTION_RESET_SECRET:
return (
<div>
<div className={styles.appActionDescription}>
<Message {...messages.allRefreshTokensWillBecomeInvalid} />{' '}
<Message {...messages.takeCareAccessTokensInvalidation} />
<div className={styles.appDetailsInfoField}>
<Input
label="Client Secret:"
skin={SKIN_LIGHT}
disabled
value={app.clientSecret}
data-testid="client-secret"
copy
/>
</div>
<div className={styles.appDetailsDescription}>
<Message {...messages.ifYouSuspectingThatSecretHasBeenCompromised} />
</div>
<div className={styles.appActionsButtons}>
{actionButtons.map(({ type, label }) => (
<Button
key={type}
label={label}
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={!!selectedAction && selectedAction !== type}
onClick={this.onActionButtonClick(type)}
small
/>
))}
</div>
<div
className={styles.appActionContainer}
ref={(el) => {
this.actionContainer = el;
}}
>
{this.getActionContent()}
</div>
</div>
</Collapse>
</div>
<div className={styles.appActionsButtons}>
<Button
label={messages.cancel}
color={COLOR_BLACK}
className={styles.appActionButton}
onClick={this.onActionButtonClick(null)}
small
/>
<div className={styles.continueActionButtonWrapper}>
{isActionPerforming ? (
<div className={styles.performingAction}>
<Message {...messages.performing} />
</div>
) : (
<div
className={styles.continueActionLink}
onClick={this.onResetSubmit(
selectedAction === ACTION_RESET_SECRET,
)}
>
<Message {...messages.continue} />
</div>
)}
</div>
</div>
</div>
);
}
case ACTION_DELETE:
return (
<div>
<div className={styles.appActionDescription}>
<Message {...messages.appAndAllTokenWillBeDeleted} />{' '}
<Message {...messages.takeCareAccessTokensInvalidation} />
</div>
<div className={styles.appActionsButtons}>
<Button
label={messages.cancel}
color={COLOR_BLACK}
className={styles.appActionButton}
onClick={this.onActionButtonClick(null)}
small
/>
<div className={styles.continueActionButtonWrapper}>
{isActionPerforming ? (
<div className={styles.performingAction}>
<Message {...messages.performing} />
</div>
) : (
<Button
label={messages.delete}
color={COLOR_RED}
className={styles.appActionButton}
onClick={this.onSubmitDelete}
data-testid="delete-app"
small
/>
)}
</div>
</div>
</div>
getActionContent() {
const { selectedAction, isActionPerforming } = this.state;
switch (selectedAction) {
case ACTION_REVOKE_TOKENS:
case ACTION_RESET_SECRET:
return (
<div>
<div className={styles.appActionDescription}>
<Message {...messages.allRefreshTokensWillBecomeInvalid} />{' '}
<Message {...messages.takeCareAccessTokensInvalidation} />
</div>
<div className={styles.appActionsButtons}>
<Button
label={messages.cancel}
color={COLOR_BLACK}
className={styles.appActionButton}
onClick={this.onActionButtonClick(null)}
small
/>
<div className={styles.continueActionButtonWrapper}>
{isActionPerforming ? (
<div className={styles.performingAction}>
<Message {...messages.performing} />
</div>
) : (
<div
className={styles.continueActionLink}
onClick={this.onResetSubmit(selectedAction === ACTION_RESET_SECRET)}
>
<Message {...messages.continue} />
</div>
)}
</div>
</div>
</div>
);
case ACTION_DELETE:
return (
<div>
<div className={styles.appActionDescription}>
<Message {...messages.appAndAllTokenWillBeDeleted} />{' '}
<Message {...messages.takeCareAccessTokensInvalidation} />
</div>
<div className={styles.appActionsButtons}>
<Button
label={messages.cancel}
color={COLOR_BLACK}
className={styles.appActionButton}
onClick={this.onActionButtonClick(null)}
small
/>
<div className={styles.continueActionButtonWrapper}>
{isActionPerforming ? (
<div className={styles.performingAction}>
<Message {...messages.performing} />
</div>
) : (
<Button
label={messages.delete}
color={COLOR_RED}
className={styles.appActionButton}
onClick={this.onSubmitDelete}
data-testid="delete-app"
small
/>
)}
</div>
</div>
</div>
);
default:
return null;
}
}
setActiveAction = (type: string | null) => {
const { actionContainer } = this;
if (!actionContainer) {
return;
}
this.setState(
{
selectedAction: type,
},
() => {
const translateY = actionContainer.offsetHeight;
this.setState({ translateY });
},
);
};
default:
return null;
}
}
onTileToggle = () => {
const { onTileClick, application } = this.props;
setActiveAction = (type: string | null) => {
const { actionContainer } = this;
onTileClick(application.clientId);
};
if (!actionContainer) {
return;
}
onCollapseRest = () => {
if (!this.props.expand && this.state.selectedAction) {
this.setActiveAction(null);
}
};
this.setState(
{
selectedAction: type,
},
() => {
const translateY = actionContainer.offsetHeight;
onActionButtonClick = (type: string | null) => () => {
this.setActiveAction(type === this.state.selectedAction ? null : type);
};
this.setState({ translateY });
},
);
};
onResetSubmit = (resetClientSecret: boolean) => async () => {
const { onResetSubmit, application } = this.props;
onTileToggle = () => {
const { onTileClick, application } = this.props;
this.setState({
isActionPerforming: true,
});
onTileClick(application.clientId);
};
await onResetSubmit(application.clientId, resetClientSecret);
onCollapseRest = () => {
if (!this.props.expand && this.state.selectedAction) {
this.setActiveAction(null);
}
};
this.setState({
isActionPerforming: false,
});
this.setActiveAction(null);
};
onActionButtonClick = (type: string | null) => () => {
this.setActiveAction(type === this.state.selectedAction ? null : type);
};
onSubmitDelete = () => {
const { onDeleteSubmit, application } = this.props;
onResetSubmit = (resetClientSecret: boolean) => async () => {
const { onResetSubmit, application } = this.props;
this.setState({
isActionPerforming: true,
});
this.setState({
isActionPerforming: true,
});
await onResetSubmit(application.clientId, resetClientSecret);
this.setState({
isActionPerforming: false,
});
this.setActiveAction(null);
};
onSubmitDelete = () => {
const { onDeleteSubmit, application } = this.props;
this.setState({
isActionPerforming: true,
});
onDeleteSubmit(application.clientId);
};
onDeleteSubmit(application.clientId);
};
}

View File

@@ -10,103 +10,94 @@ import styles from '../applicationsIndex.scss';
import ApplicationItem from './ApplicationItem';
type Props = {
applications: OauthAppResponse[];
deleteApp: (clientId: string) => Promise<any>;
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
resetClientId: () => void;
clientId: string | null;
applications: OauthAppResponse[];
deleteApp: (clientId: string) => Promise<any>;
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
resetClientId: () => void;
clientId: string | null;
};
type State = {
expandedApp: string | null;
expandedApp: string | null;
};
export default class ApplicationsList extends React.Component<Props, State> {
state = {
expandedApp: null,
};
state = {
expandedApp: null,
};
appsRefs: { [key: string]: HTMLDivElement | null } = {};
appsRefs: { [key: string]: HTMLDivElement | null } = {};
componentDidMount() {
this.checkForActiveApp();
}
componentDidMount() {
this.checkForActiveApp();
}
componentDidUpdate() {
this.checkForActiveApp();
}
componentDidUpdate() {
this.checkForActiveApp();
}
render() {
const { applications, resetApp, deleteApp } = this.props;
const { expandedApp } = this.state;
render() {
const { applications, resetApp, deleteApp } = this.props;
const { expandedApp } = this.state;
return (
<div>
<div className={styles.appsListTitleContainer}>
<div className={styles.appsListTitle}>
<Message {...messages.yourApplications} />
</div>
<LinkButton
to="/dev/applications/new"
data-e2e="newApp"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.appsListAddNewAppBtn}
/>
</div>
<div className={styles.appsListContainer}>
{applications.map((app) => (
<div
key={app.clientId}
ref={(elem) => {
this.appsRefs[app.clientId] = elem;
}}
>
<ApplicationItem
application={app}
expand={app.clientId === expandedApp}
onTileClick={this.onTileClick}
onResetSubmit={resetApp}
onDeleteSubmit={deleteApp}
/>
return (
<div>
<div className={styles.appsListTitleContainer}>
<div className={styles.appsListTitle}>
<Message {...messages.yourApplications} />
</div>
<LinkButton
to="/dev/applications/new"
data-e2e="newApp"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.appsListAddNewAppBtn}
/>
</div>
<div className={styles.appsListContainer}>
{applications.map((app) => (
<div
key={app.clientId}
ref={(elem) => {
this.appsRefs[app.clientId] = elem;
}}
>
<ApplicationItem
application={app}
expand={app.clientId === expandedApp}
onTileClick={this.onTileClick}
onResetSubmit={resetApp}
onDeleteSubmit={deleteApp}
/>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}
checkForActiveApp() {
const { applications, clientId } = this.props;
const { expandedApp } = this.state;
if (
clientId &&
expandedApp !== clientId &&
applications.some((app) => app.clientId === clientId)
) {
requestAnimationFrame(() =>
this.onTileClick(clientId, { noReset: true }),
);
}
}
onTileClick = (
clientId: string,
{ noReset = false }: { noReset?: boolean } = {},
) => {
const { clientId: initialClientId, resetClientId } = this.props;
const expandedApp = this.state.expandedApp === clientId ? null : clientId;
if (initialClientId && noReset !== true) {
resetClientId();
);
}
this.setState({ expandedApp }, () => {
if (expandedApp !== null) {
// TODO: @SleepWalker: мб у тебя есть идея, как это сделать более правильно и менее дёргано?
setTimeout(() => restoreScroll(this.appsRefs[clientId]), 150);
}
});
};
checkForActiveApp() {
const { applications, clientId } = this.props;
const { expandedApp } = this.state;
if (clientId && expandedApp !== clientId && applications.some((app) => app.clientId === clientId)) {
requestAnimationFrame(() => this.onTileClick(clientId, { noReset: true }));
}
}
onTileClick = (clientId: string, { noReset = false }: { noReset?: boolean } = {}) => {
const { clientId: initialClientId, resetClientId } = this.props;
const expandedApp = this.state.expandedApp === clientId ? null : clientId;
if (initialClientId && noReset !== true) {
resetClientId();
}
this.setState({ expandedApp }, () => {
if (expandedApp !== null) {
// TODO: @SleepWalker: мб у тебя есть идея, как это сделать более правильно и менее дёргано?
setTimeout(() => restoreScroll(this.appsRefs[clientId]), 150);
}
});
};
}

View File

@@ -3,48 +3,44 @@ import { OauthAppResponse } from 'app/services/api/oauth';
import { Action } from './actions';
export interface Apps {
available: Array<OauthAppResponse>;
available: Array<OauthAppResponse>;
}
const defaults: Apps = {
available: [],
available: [],
};
export default function apps(state: Apps = defaults, action: Action): Apps {
switch (action.type) {
case 'apps:setAvailable':
return {
...state,
available: action.payload,
};
switch (action.type) {
case 'apps:setAvailable':
return {
...state,
available: action.payload,
};
case 'apps:addApp': {
const { payload } = action;
const available = [...state.available];
let index = available.findIndex(
(app) => app.clientId === payload.clientId,
);
case 'apps:addApp': {
const { payload } = action;
const available = [...state.available];
let index = available.findIndex((app) => app.clientId === payload.clientId);
if (index === -1) {
index = available.length;
}
if (index === -1) {
index = available.length;
}
available[index] = action.payload;
available[index] = action.payload;
return {
...state,
available,
};
return {
...state,
available,
};
}
case 'apps:deleteApp':
return {
...state,
available: state.available.filter((app) => app.clientId !== action.payload),
};
}
case 'apps:deleteApp':
return {
...state,
available: state.available.filter(
(app) => app.clientId !== action.payload,
),
};
}
return state;
return state;
}