refactor(web): remove typescript
This commit is contained in:
@@ -1,18 +1,10 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IApp } from 'types';
|
||||
|
||||
import { GET_APP } from 'graphql/queries/get-app';
|
||||
|
||||
type QueryResponse = {
|
||||
getApp: IApp;
|
||||
};
|
||||
|
||||
export default function useApp(key: string) {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_APP, {
|
||||
export default function useApp(key) {
|
||||
const { data, loading } = useQuery(GET_APP, {
|
||||
variables: { key },
|
||||
});
|
||||
const app = data?.getApp;
|
||||
|
||||
return {
|
||||
app,
|
||||
loading,
|
@@ -1,27 +1,17 @@
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { AppAuthClient } from 'types';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GET_APP_AUTH_CLIENT } from 'graphql/queries/get-app-auth-client.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getAppAuthClient: AppAuthClient;
|
||||
};
|
||||
|
||||
export default function useAppAuthClient(id?: string) {
|
||||
export default function useAppAuthClient(id) {
|
||||
const [getAppAuthClient, { data, loading }] =
|
||||
useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENT);
|
||||
useLazyQuery(GET_APP_AUTH_CLIENT);
|
||||
const appAuthClient = data?.getAppAuthClient;
|
||||
|
||||
React.useEffect(
|
||||
function fetchUponId() {
|
||||
if (!id) return;
|
||||
|
||||
getAppAuthClient({ variables: { id } });
|
||||
},
|
||||
[id]
|
||||
[id],
|
||||
);
|
||||
|
||||
return {
|
||||
appAuthClient,
|
||||
loading,
|
@@ -1,39 +1,23 @@
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { AppAuthClient } from 'types';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GET_APP_AUTH_CLIENTS } from 'graphql/queries/get-app-auth-clients.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getAppAuthClients: AppAuthClient[];
|
||||
};
|
||||
|
||||
export default function useAppAuthClient({
|
||||
appKey,
|
||||
active,
|
||||
}: {
|
||||
appKey: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
const [getAppAuthClients, { data, loading }] = useLazyQuery<QueryResponse>(
|
||||
export default function useAppAuthClient({ appKey, active }) {
|
||||
const [getAppAuthClients, { data, loading }] = useLazyQuery(
|
||||
GET_APP_AUTH_CLIENTS,
|
||||
{
|
||||
context: { autoSnackbar: false },
|
||||
}
|
||||
},
|
||||
);
|
||||
const appAuthClients = data?.getAppAuthClients;
|
||||
|
||||
React.useEffect(
|
||||
function fetchUponAppKey() {
|
||||
if (!appKey) return;
|
||||
|
||||
getAppAuthClients({
|
||||
variables: { appKey, ...(typeof active === 'boolean' && { active }) },
|
||||
});
|
||||
},
|
||||
[appKey]
|
||||
[appKey],
|
||||
);
|
||||
|
||||
return {
|
||||
appAuthClients,
|
||||
loading,
|
@@ -1,19 +1,11 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { AppConfig } from 'types';
|
||||
|
||||
import { GET_APP_CONFIG } from 'graphql/queries/get-app-config.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getAppConfig: AppConfig;
|
||||
};
|
||||
|
||||
export default function useAppConfig(key: string) {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_APP_CONFIG, {
|
||||
export default function useAppConfig(key) {
|
||||
const { data, loading } = useQuery(GET_APP_CONFIG, {
|
||||
variables: { key },
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
const appConfig = data?.getAppConfig;
|
||||
|
||||
return {
|
||||
appConfig,
|
||||
loading,
|
12
packages/web/src/hooks/useApps.js
Normal file
12
packages/web/src/hooks/useApps.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||
export default function useApps(variables) {
|
||||
const { data, loading } = useQuery(GET_APPS, {
|
||||
variables,
|
||||
});
|
||||
const apps = data?.getApps;
|
||||
return {
|
||||
apps,
|
||||
loading,
|
||||
};
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IApp } from 'types';
|
||||
|
||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||
|
||||
type QueryResponse = {
|
||||
getApps: IApp[];
|
||||
};
|
||||
|
||||
type UseAppsVariables = {
|
||||
name?: string;
|
||||
onlyWithTriggers?: boolean;
|
||||
onlyWithActions?: boolean;
|
||||
};
|
||||
|
||||
export default function useApps(variables?: UseAppsVariables) {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_APPS, {
|
||||
variables,
|
||||
});
|
||||
const apps = data?.getApps;
|
||||
|
||||
return {
|
||||
apps,
|
||||
loading,
|
||||
};
|
||||
}
|
@@ -1,57 +1,31 @@
|
||||
import { IApp } from 'types';
|
||||
import * as React from 'react';
|
||||
|
||||
import { processStep } from 'helpers/authenticationSteps';
|
||||
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
|
||||
import useApp from './useApp';
|
||||
|
||||
type UseAuthenticateAppParams = {
|
||||
appKey: string;
|
||||
appAuthClientId?: string;
|
||||
useShared?: boolean;
|
||||
connectionId?: string;
|
||||
};
|
||||
|
||||
type AuthenticatePayload = {
|
||||
fields?: Record<string, string>;
|
||||
appAuthClientId?: string;
|
||||
};
|
||||
|
||||
function getSteps(
|
||||
auth: IApp['auth'],
|
||||
hasConnection: boolean,
|
||||
useShared: boolean
|
||||
) {
|
||||
function getSteps(auth, hasConnection, useShared) {
|
||||
if (hasConnection) {
|
||||
if (useShared) {
|
||||
return auth?.sharedReconnectionSteps;
|
||||
}
|
||||
|
||||
return auth?.reconnectionSteps;
|
||||
}
|
||||
|
||||
if (useShared) {
|
||||
return auth?.sharedAuthenticationSteps;
|
||||
}
|
||||
|
||||
return auth?.authenticationSteps;
|
||||
}
|
||||
|
||||
export default function useAuthenticateApp(payload: UseAuthenticateAppParams) {
|
||||
export default function useAuthenticateApp(payload) {
|
||||
const { appKey, appAuthClientId, connectionId, useShared = false } = payload;
|
||||
const { app } = useApp(appKey);
|
||||
const [authenticationInProgress, setAuthenticationInProgress] =
|
||||
React.useState(false);
|
||||
const steps = getSteps(app?.auth, !!connectionId, useShared);
|
||||
|
||||
const authenticate = React.useMemo(() => {
|
||||
if (!steps?.length) return;
|
||||
|
||||
return async function authenticate(payload: AuthenticatePayload = {}) {
|
||||
return async function authenticate(payload = {}) {
|
||||
const { fields } = payload;
|
||||
setAuthenticationInProgress(true);
|
||||
|
||||
const response: Record<string, any> = {
|
||||
const response = {
|
||||
key: appKey,
|
||||
appAuthClientId: appAuthClientId || payload.appAuthClientId,
|
||||
connection: {
|
||||
@@ -59,35 +33,26 @@ export default function useAuthenticateApp(payload: UseAuthenticateAppParams) {
|
||||
},
|
||||
fields,
|
||||
};
|
||||
|
||||
let stepIndex = 0;
|
||||
while (stepIndex < steps?.length) {
|
||||
const step = steps[stepIndex];
|
||||
const variables = computeAuthStepVariables(step.arguments, response);
|
||||
|
||||
try {
|
||||
const stepResponse = await processStep(step, variables);
|
||||
|
||||
response[step.name] = stepResponse;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
|
||||
setAuthenticationInProgress(false);
|
||||
break;
|
||||
throw err;
|
||||
}
|
||||
|
||||
stepIndex++;
|
||||
|
||||
if (stepIndex === steps.length) {
|
||||
return response;
|
||||
}
|
||||
|
||||
setAuthenticationInProgress(false);
|
||||
}
|
||||
};
|
||||
}, [steps, appKey, appAuthClientId, connectionId]);
|
||||
|
||||
return {
|
||||
authenticate,
|
||||
inProgress: authenticationInProgress,
|
@@ -1,16 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { AuthenticationContext } from 'contexts/Authentication';
|
||||
import type { AuthenticationContextParams } from 'contexts/Authentication';
|
||||
|
||||
type UseAuthenticationReturn = {
|
||||
isAuthenticated: boolean;
|
||||
token: AuthenticationContextParams['token'];
|
||||
updateToken: AuthenticationContextParams['updateToken'];
|
||||
};
|
||||
|
||||
export default function useAuthentication(): UseAuthenticationReturn {
|
||||
export default function useAuthentication() {
|
||||
const authenticationContext = React.useContext(AuthenticationContext);
|
||||
|
||||
return {
|
||||
token: authenticationContext.token,
|
||||
updateToken: authenticationContext.updateToken,
|
@@ -1,15 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { AutomatischInfoContext } from 'contexts/AutomatischInfo';
|
||||
|
||||
type UseAutomatischInfoReturn = {
|
||||
isCloud: boolean;
|
||||
isMation: boolean;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function useAutomatischInfo(): UseAutomatischInfoReturn {
|
||||
export default function useAutomatischInfo() {
|
||||
const automatischInfoContext = React.useContext(AutomatischInfoContext);
|
||||
|
||||
return {
|
||||
isCloud: automatischInfoContext.isCloud,
|
||||
isMation: automatischInfoContext.isMation,
|
@@ -2,22 +2,16 @@ import * as React from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { DateTime } from 'luxon';
|
||||
import { TSubscription } from 'types';
|
||||
|
||||
import { GET_BILLING_AND_USAGE } from 'graphql/queries/get-billing-and-usage.ee';
|
||||
|
||||
function transform(
|
||||
billingAndUsageData: NonNullable<UseBillingAndUsageDataReturn>
|
||||
) {
|
||||
function transform(billingAndUsageData) {
|
||||
const nextBillDate = billingAndUsageData.subscription.nextBillDate;
|
||||
const nextBillDateTitle = nextBillDate.title;
|
||||
const nextBillDateTitleDateObject = DateTime.fromMillis(
|
||||
Number(nextBillDateTitle)
|
||||
Number(nextBillDateTitle),
|
||||
);
|
||||
const formattedNextBillDateTitle = nextBillDateTitleDateObject.isValid
|
||||
? nextBillDateTitleDateObject.toFormat('LLL dd, yyyy')
|
||||
: nextBillDateTitle;
|
||||
|
||||
return {
|
||||
...billingAndUsageData,
|
||||
subscription: {
|
||||
@@ -29,42 +23,30 @@ function transform(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type UseBillingAndUsageDataReturn = {
|
||||
subscription: TSubscription;
|
||||
usage: {
|
||||
task: number;
|
||||
};
|
||||
} | null;
|
||||
|
||||
export default function useBillingAndUsageData(): UseBillingAndUsageDataReturn {
|
||||
export default function useBillingAndUsageData() {
|
||||
const location = useLocation();
|
||||
const state = location.state as { checkoutCompleted: boolean };
|
||||
const state = location.state;
|
||||
const { data, loading, startPolling, stopPolling } = useQuery(
|
||||
GET_BILLING_AND_USAGE
|
||||
GET_BILLING_AND_USAGE,
|
||||
);
|
||||
const checkoutCompleted = state?.checkoutCompleted;
|
||||
const hasSubscription = !!data?.getBillingAndUsage?.subscription?.status;
|
||||
|
||||
React.useEffect(
|
||||
function pollDataUntilSubscriptionIsCreated() {
|
||||
if (checkoutCompleted && !hasSubscription) {
|
||||
startPolling(1000);
|
||||
}
|
||||
},
|
||||
[checkoutCompleted, hasSubscription, startPolling]
|
||||
[checkoutCompleted, hasSubscription, startPolling],
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
function stopPollingWhenSubscriptionIsCreated() {
|
||||
if (checkoutCompleted && hasSubscription) {
|
||||
stopPolling();
|
||||
}
|
||||
},
|
||||
[checkoutCompleted, hasSubscription, stopPolling]
|
||||
[checkoutCompleted, hasSubscription, stopPolling],
|
||||
);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return transform(data.getBillingAndUsage);
|
||||
}
|
@@ -1,20 +1,11 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import useAutomatischInfo from './useAutomatischInfo';
|
||||
|
||||
type UseCloudOptions = {
|
||||
redirect?: boolean;
|
||||
}
|
||||
|
||||
export default function useCloud(options?: UseCloudOptions): boolean {
|
||||
export default function useCloud(options) {
|
||||
const redirect = options?.redirect || false;
|
||||
|
||||
const { isCloud } = useAutomatischInfo();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isCloud === false && redirect) {
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
return isCloud;
|
||||
}
|
11
packages/web/src/hooks/useConfig.js
Normal file
11
packages/web/src/hooks/useConfig.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_CONFIG } from 'graphql/queries/get-config.ee';
|
||||
export default function useConfig(keys) {
|
||||
const { data, loading } = useQuery(GET_CONFIG, {
|
||||
variables: { keys },
|
||||
});
|
||||
return {
|
||||
config: data?.getConfig,
|
||||
loading,
|
||||
};
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IJSONObject } from 'types';
|
||||
|
||||
import { GET_CONFIG } from 'graphql/queries/get-config.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getConfig: IJSONObject;
|
||||
};
|
||||
|
||||
export default function useConfig(keys?: string[]) {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_CONFIG, {
|
||||
variables: { keys },
|
||||
});
|
||||
|
||||
return {
|
||||
config: data?.getConfig,
|
||||
loading,
|
||||
};
|
||||
}
|
@@ -1,10 +1,6 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IUser } from 'types';
|
||||
|
||||
import { GET_CURRENT_USER } from 'graphql/queries/get-current-user';
|
||||
|
||||
export default function useCurrentUser(): IUser {
|
||||
export default function useCurrentUser() {
|
||||
const { data } = useQuery(GET_CURRENT_USER);
|
||||
|
||||
return data?.getCurrentUser;
|
||||
}
|
@@ -1,8 +1,6 @@
|
||||
import userAbility from 'helpers/userAbility';
|
||||
import useCurrentUser from 'hooks/useCurrentUser';
|
||||
|
||||
export default function useCurrentUserAbility() {
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
return userAbility(currentUser);
|
||||
}
|
@@ -2,40 +2,25 @@ import * as React from 'react';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import set from 'lodash/set';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import type { IField, IFieldDropdownSource, IJSONObject } from 'types';
|
||||
|
||||
import { GET_DYNAMIC_DATA } from 'graphql/queries/get-dynamic-data';
|
||||
|
||||
const variableRegExp = /({.*?})/;
|
||||
|
||||
function computeArguments(
|
||||
args: IFieldDropdownSource['arguments'],
|
||||
getValues: UseFormReturn['getValues']
|
||||
): IJSONObject {
|
||||
function computeArguments(args, getValues) {
|
||||
const initialValue = {};
|
||||
return args.reduce((result, { name, value }) => {
|
||||
const isVariable = variableRegExp.test(value);
|
||||
|
||||
if (isVariable) {
|
||||
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||
const computedValue = getValues(sanitizedFieldPath);
|
||||
|
||||
if (computedValue === undefined)
|
||||
throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||
|
||||
set(result, name, computedValue);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
set(result, name, value);
|
||||
|
||||
return result;
|
||||
}, initialValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the dynamic data for the given step.
|
||||
* This hook must be within a react-hook-form context.
|
||||
@@ -43,13 +28,12 @@ function computeArguments(
|
||||
* @param stepId - the id of the step
|
||||
* @param schema - the field that needs the dynamic data
|
||||
*/
|
||||
function useDynamicData(stepId: string | undefined, schema: IField) {
|
||||
function useDynamicData(stepId, schema) {
|
||||
const lastComputedVariables = React.useRef({});
|
||||
const [getDynamicData, { called, data, loading }] =
|
||||
useLazyQuery(GET_DYNAMIC_DATA);
|
||||
const { getValues } = useFormContext();
|
||||
const formValues = getValues();
|
||||
|
||||
/**
|
||||
* Return `null` when even a field is missing value.
|
||||
*
|
||||
@@ -60,27 +44,22 @@ function useDynamicData(stepId: string | undefined, schema: IField) {
|
||||
if (schema.type === 'dropdown' && schema.source) {
|
||||
try {
|
||||
const variables = computeArguments(schema.source.arguments, getValues);
|
||||
|
||||
// if computed variables are the same, return the last computed variables.
|
||||
if (isEqual(variables, lastComputedVariables.current)) {
|
||||
return lastComputedVariables.current;
|
||||
}
|
||||
|
||||
lastComputedVariables.current = variables;
|
||||
|
||||
return variables;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
/**
|
||||
* `formValues` is to trigger recomputation when form is updated.
|
||||
* `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`.
|
||||
*/
|
||||
}, [schema, formValues, getValues]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
schema.type === 'dropdown' &&
|
||||
@@ -96,12 +75,10 @@ function useDynamicData(stepId: string | undefined, schema: IField) {
|
||||
});
|
||||
}
|
||||
}, [getDynamicData, stepId, schema, computedVariables]);
|
||||
|
||||
return {
|
||||
called,
|
||||
data: data?.getDynamicData,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
export default useDynamicData;
|
@@ -1,46 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import set from 'lodash/set';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import type {
|
||||
IField,
|
||||
IFieldDropdownAdditionalFields,
|
||||
IJSONObject,
|
||||
} from 'types';
|
||||
|
||||
import { GET_DYNAMIC_FIELDS } from 'graphql/queries/get-dynamic-fields';
|
||||
|
||||
const variableRegExp = /({.*?})/;
|
||||
|
||||
// TODO: extract this function to a separate file
|
||||
function computeArguments(
|
||||
args: IFieldDropdownAdditionalFields['arguments'],
|
||||
getValues: UseFormReturn['getValues']
|
||||
): IJSONObject {
|
||||
function computeArguments(args, getValues) {
|
||||
const initialValue = {};
|
||||
return args.reduce((result, { name, value }) => {
|
||||
const isVariable = variableRegExp.test(value);
|
||||
|
||||
if (isVariable) {
|
||||
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||
const computedValue = getValues(sanitizedFieldPath);
|
||||
|
||||
if (computedValue === undefined || computedValue === '')
|
||||
throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||
|
||||
set(result, name, computedValue);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
set(result, name, value);
|
||||
|
||||
return result;
|
||||
}, initialValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the dynamic fields for the given step.
|
||||
* This hook must be within a react-hook-form context.
|
||||
@@ -48,13 +29,12 @@ function computeArguments(
|
||||
* @param stepId - the id of the step
|
||||
* @param schema - the field schema that needs the dynamic fields
|
||||
*/
|
||||
function useDynamicFields(stepId: string | undefined, schema: IField) {
|
||||
function useDynamicFields(stepId, schema) {
|
||||
const lastComputedVariables = React.useRef({});
|
||||
const [getDynamicFields, { called, data, loading }] =
|
||||
useLazyQuery(GET_DYNAMIC_FIELDS);
|
||||
const { getValues } = useFormContext();
|
||||
const formValues = getValues();
|
||||
|
||||
/**
|
||||
* Return `null` when even a field is missing value.
|
||||
*
|
||||
@@ -66,29 +46,24 @@ function useDynamicFields(stepId: string | undefined, schema: IField) {
|
||||
try {
|
||||
const variables = computeArguments(
|
||||
schema.additionalFields.arguments,
|
||||
getValues
|
||||
getValues,
|
||||
);
|
||||
|
||||
// if computed variables are the same, return the last computed variables.
|
||||
if (isEqual(variables, lastComputedVariables.current)) {
|
||||
return lastComputedVariables.current;
|
||||
}
|
||||
|
||||
lastComputedVariables.current = variables;
|
||||
|
||||
return variables;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
/**
|
||||
* `formValues` is to trigger recomputation when form is updated.
|
||||
* `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`.
|
||||
*/
|
||||
}, [schema, formValues, getValues]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
schema.type === 'dropdown' &&
|
||||
@@ -104,12 +79,10 @@ function useDynamicFields(stepId: string | undefined, schema: IField) {
|
||||
});
|
||||
}
|
||||
}, [getDynamicFields, stepId, schema, computedVariables]);
|
||||
|
||||
return {
|
||||
called,
|
||||
data: data?.getDynamicFields as IField[] | undefined,
|
||||
data: data?.getDynamicFields,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
export default useDynamicFields;
|
18
packages/web/src/hooks/useEnqueueSnackbar.js
Normal file
18
packages/web/src/hooks/useEnqueueSnackbar.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useSnackbar } from 'notistack';
|
||||
export default function useEnqueueSnackbar() {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
return function wrappedEnqueueSnackbar(message, options) {
|
||||
const key = enqueueSnackbar(message, {
|
||||
...(options || {}),
|
||||
SnackbarProps: {
|
||||
onClick: () => closeSnackbar(key),
|
||||
...{
|
||||
'data-test': 'snackbar',
|
||||
'data-snackbar-variant': `${options.variant}` || 'default',
|
||||
},
|
||||
...(options.SnackbarProps || {}),
|
||||
},
|
||||
});
|
||||
return key;
|
||||
};
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
import type {
|
||||
OptionsWithExtraProps,
|
||||
SnackbarMessage,
|
||||
VariantType,
|
||||
SnackbarKey
|
||||
} from 'notistack';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
type ExtendedOptionsWithExtraProps<V extends VariantType> = OptionsWithExtraProps<V> & {
|
||||
SnackbarProps?: OptionsWithExtraProps<V> & { 'data-test'?: string; }
|
||||
}
|
||||
|
||||
export default function useEnqueueSnackbar() {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
|
||||
return function wrappedEnqueueSnackbar<V extends VariantType>(message: SnackbarMessage, options: ExtendedOptionsWithExtraProps<V>) {
|
||||
const key: SnackbarKey = enqueueSnackbar(
|
||||
message,
|
||||
{
|
||||
...(options || {}) as Record<string, unknown>,
|
||||
SnackbarProps: {
|
||||
onClick: () => closeSnackbar(key),
|
||||
...({
|
||||
'data-test': 'snackbar', // keep above options.snackbarProps
|
||||
'data-snackbar-variant': `${options.variant}` || 'default',
|
||||
}) as Record<string, string>,
|
||||
...(options.SnackbarProps || {}) as Record<string, unknown>,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return key;
|
||||
};
|
||||
}
|
5
packages/web/src/hooks/useFormatMessage.js
Normal file
5
packages/web/src/hooks/useFormatMessage.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useIntl } from 'react-intl';
|
||||
export default function useFormatMessage() {
|
||||
const { formatMessage } = useIntl();
|
||||
return (id, values = {}) => formatMessage({ id }, values);
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
type Values = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export default function useFormatMessage(): (
|
||||
id: string,
|
||||
values?: Values
|
||||
) => string {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (id: string, values: Values = {}) => formatMessage({ id }, values);
|
||||
}
|
@@ -1,16 +1,7 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { TInvoice } from 'types';
|
||||
|
||||
import { GET_INVOICES } from 'graphql/queries/get-invoices.ee';
|
||||
|
||||
type UseInvoicesReturn = {
|
||||
invoices: TInvoice[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function useInvoices(): UseInvoicesReturn {
|
||||
export default function useInvoices() {
|
||||
const { data, loading } = useQuery(GET_INVOICES);
|
||||
|
||||
return {
|
||||
invoices: data?.getInvoices || [],
|
||||
loading: loading,
|
@@ -1,18 +1,8 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import type { Notification } from 'types';
|
||||
|
||||
import { GET_NOTIFICATIONS } from 'graphql/queries/get-notifications';
|
||||
|
||||
type UseNotificationsReturn = {
|
||||
notifications: Notification[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function useNotifications(): UseNotificationsReturn {
|
||||
export default function useNotifications() {
|
||||
const { data, loading } = useQuery(GET_NOTIFICATIONS);
|
||||
|
||||
const notifications = data?.getNotifications || [];
|
||||
|
||||
return {
|
||||
loading,
|
||||
notifications,
|
@@ -1,13 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { PaddleContext } from 'contexts/Paddle.ee';
|
||||
|
||||
type UsePaddleReturn = {
|
||||
loaded: boolean;
|
||||
};
|
||||
|
||||
export default function usePaddle(): UsePaddleReturn {
|
||||
export default function usePaddle() {
|
||||
const paddleContext = React.useContext(PaddleContext);
|
||||
|
||||
return {
|
||||
loaded: paddleContext.loaded,
|
||||
};
|
@@ -1,19 +1,10 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { GET_PADDLE_INFO } from 'graphql/queries/get-paddle-info.ee';
|
||||
|
||||
type UsePaddleInfoReturn = {
|
||||
sandbox: boolean;
|
||||
vendorId: string;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function usePaddleInfo(): UsePaddleInfoReturn {
|
||||
export default function usePaddleInfo() {
|
||||
const { data, loading } = useQuery(GET_PADDLE_INFO);
|
||||
|
||||
return {
|
||||
sandbox: data?.getPaddleInfo?.sandbox,
|
||||
vendorId: data?.getPaddleInfo?.vendorId,
|
||||
loading
|
||||
loading,
|
||||
};
|
||||
}
|
@@ -1,16 +1,7 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { TPaymentPlan } from 'types';
|
||||
import { GET_PAYMENT_PLANS } from 'graphql/queries/get-payment-plans.ee';
|
||||
|
||||
type UsePaymentPlansReturn = {
|
||||
plans: TPaymentPlan[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function usePaymentPlans(): UsePaymentPlansReturn {
|
||||
export default function usePaymentPlans() {
|
||||
const { data, loading } = useQuery(GET_PAYMENT_PLANS);
|
||||
|
||||
return {
|
||||
plans: data?.getPaymentPlans || [],
|
||||
loading,
|
@@ -1,15 +1,6 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IPermissionCatalog } from 'types';
|
||||
|
||||
import { GET_PERMISSION_CATALOG } from 'graphql/queries/get-permission-catalog.ee';
|
||||
|
||||
type UsePermissionCatalogReturn = {
|
||||
permissionCatalog: IPermissionCatalog;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function usePermissionCatalog(): UsePermissionCatalogReturn {
|
||||
export default function usePermissionCatalog() {
|
||||
const { data, loading } = useQuery(GET_PERMISSION_CATALOG);
|
||||
|
||||
return { permissionCatalog: data?.getPermissionCatalog, loading };
|
||||
}
|
@@ -1,16 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { IRole } from 'types';
|
||||
|
||||
import { GET_ROLE } from 'graphql/queries/get-role.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getRole: IRole;
|
||||
};
|
||||
|
||||
export default function useRole(roleId?: string) {
|
||||
const [getRole, { data, loading }] = useLazyQuery<QueryResponse>(GET_ROLE);
|
||||
|
||||
export default function useRole(roleId) {
|
||||
const [getRole, { data, loading }] = useLazyQuery(GET_ROLE);
|
||||
React.useEffect(() => {
|
||||
if (roleId) {
|
||||
getRole({
|
||||
@@ -20,7 +12,6 @@ export default function useRole(roleId?: string) {
|
||||
});
|
||||
}
|
||||
}, [roleId]);
|
||||
|
||||
return {
|
||||
role: data?.getRole,
|
||||
loading,
|
@@ -1,17 +1,9 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IRole } from 'types';
|
||||
|
||||
import { GET_ROLES } from 'graphql/queries/get-roles.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getRoles: IRole[];
|
||||
};
|
||||
|
||||
export default function useRoles() {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_ROLES, {
|
||||
const { data, loading } = useQuery(GET_ROLES, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
|
||||
return {
|
||||
roles: data?.getRoles || [],
|
||||
loading,
|
12
packages/web/src/hooks/useSamlAuthProvider.js
Normal file
12
packages/web/src/hooks/useSamlAuthProvider.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_SAML_AUTH_PROVIDER } from 'graphql/queries/get-saml-auth-provider';
|
||||
export default function useSamlAuthProvider() {
|
||||
const { data, loading, refetch } = useQuery(GET_SAML_AUTH_PROVIDER, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
return {
|
||||
provider: data?.getSamlAuthProvider,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
import { QueryResult, useQuery } from '@apollo/client';
|
||||
|
||||
import { TSamlAuthProvider } from 'types';
|
||||
import { GET_SAML_AUTH_PROVIDER } from 'graphql/queries/get-saml-auth-provider';
|
||||
|
||||
type UseSamlAuthProviderReturn = {
|
||||
provider?: TSamlAuthProvider;
|
||||
loading: boolean;
|
||||
refetch: QueryResult<TSamlAuthProvider | undefined>['refetch'];
|
||||
};
|
||||
|
||||
export default function useSamlAuthProvider(): UseSamlAuthProviderReturn {
|
||||
const { data, loading, refetch } = useQuery(GET_SAML_AUTH_PROVIDER, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
|
||||
return {
|
||||
provider: data?.getSamlAuthProvider,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
}
|
@@ -1,17 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { TSamlAuthProviderRole } from 'types';
|
||||
|
||||
import { GET_SAML_AUTH_PROVIDER_ROLE_MAPPINGS } from 'graphql/queries/get-saml-auth-provider-role-mappings';
|
||||
|
||||
type QueryResponse = {
|
||||
getSamlAuthProviderRoleMappings: TSamlAuthProviderRole[];
|
||||
};
|
||||
|
||||
export default function useSamlAuthProviderRoleMappings(providerId?: string) {
|
||||
const [getSamlAuthProviderRoleMappings, { data, loading }] =
|
||||
useLazyQuery<QueryResponse>(GET_SAML_AUTH_PROVIDER_ROLE_MAPPINGS);
|
||||
|
||||
export default function useSamlAuthProviderRoleMappings(providerId) {
|
||||
const [getSamlAuthProviderRoleMappings, { data, loading }] = useLazyQuery(
|
||||
GET_SAML_AUTH_PROVIDER_ROLE_MAPPINGS,
|
||||
);
|
||||
React.useEffect(() => {
|
||||
if (providerId) {
|
||||
getSamlAuthProviderRoleMappings({
|
||||
@@ -21,7 +14,6 @@ export default function useSamlAuthProviderRoleMappings(providerId?: string) {
|
||||
});
|
||||
}
|
||||
}, [providerId]);
|
||||
|
||||
return {
|
||||
roleMappings: data?.getSamlAuthProviderRoleMappings || [],
|
||||
loading,
|
@@ -1,16 +1,7 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { TSamlAuthProvider } from 'types';
|
||||
import { LIST_SAML_AUTH_PROVIDERS } from 'graphql/queries/list-saml-auth-providers.ee';
|
||||
|
||||
type UseSamlAuthProvidersReturn = {
|
||||
providers: TSamlAuthProvider[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function useSamlAuthProviders(): UseSamlAuthProvidersReturn {
|
||||
export default function useSamlAuthProviders() {
|
||||
const { data, loading } = useQuery(LIST_SAML_AUTH_PROVIDERS);
|
||||
|
||||
return {
|
||||
providers: data?.listSamlAuthProviders || [],
|
||||
loading,
|
21
packages/web/src/hooks/useSubscriptionStatus.ee.js
Normal file
21
packages/web/src/hooks/useSubscriptionStatus.ee.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { GET_SUBSCRIPTION_STATUS } from 'graphql/queries/get-subscription-status.ee';
|
||||
import useFormatMessage from './useFormatMessage';
|
||||
export default function useSubscriptionStatus() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { data, loading } = useQuery(GET_SUBSCRIPTION_STATUS);
|
||||
const cancellationEffectiveDate =
|
||||
data?.getSubscriptionStatus?.cancellationEffectiveDate;
|
||||
const hasCancelled = !!cancellationEffectiveDate;
|
||||
if (loading || !hasCancelled) return null;
|
||||
const cancellationEffectiveDateObject = DateTime.fromMillis(
|
||||
Number(cancellationEffectiveDate),
|
||||
).startOf('day');
|
||||
return {
|
||||
message: formatMessage('subscriptionCancelledAlert.text', {
|
||||
date: cancellationEffectiveDateObject.toFormat('DDD'),
|
||||
}),
|
||||
cancellationEffectiveDate: cancellationEffectiveDateObject,
|
||||
};
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { GET_SUBSCRIPTION_STATUS } from 'graphql/queries/get-subscription-status.ee';
|
||||
import useFormatMessage from './useFormatMessage';
|
||||
|
||||
type UseSubscriptionStatusReturn = {
|
||||
cancellationEffectiveDate: DateTime;
|
||||
message: string;
|
||||
} | null;
|
||||
|
||||
export default function useSubscriptionStatus(): UseSubscriptionStatusReturn {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { data, loading, } = useQuery(GET_SUBSCRIPTION_STATUS);
|
||||
const cancellationEffectiveDate = data?.getSubscriptionStatus?.cancellationEffectiveDate as string;
|
||||
const hasCancelled = !!cancellationEffectiveDate;
|
||||
|
||||
if (loading || !hasCancelled) return null;
|
||||
|
||||
const cancellationEffectiveDateObject = DateTime.fromMillis(Number(cancellationEffectiveDate)).startOf('day');
|
||||
|
||||
return {
|
||||
message: formatMessage(
|
||||
'subscriptionCancelledAlert.text',
|
||||
{
|
||||
date: cancellationEffectiveDateObject.toFormat('DDD')
|
||||
}
|
||||
),
|
||||
cancellationEffectiveDate: cancellationEffectiveDateObject,
|
||||
};
|
||||
}
|
@@ -2,87 +2,73 @@ import * as React from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { GET_TRIAL_STATUS } from 'graphql/queries/get-trial-status.ee';
|
||||
import useFormatMessage from './useFormatMessage';
|
||||
|
||||
type UseTrialStatusReturn = {
|
||||
expireAt: DateTime;
|
||||
message: string;
|
||||
over: boolean;
|
||||
status: 'error' | 'warning';
|
||||
} | null;
|
||||
|
||||
function getDiffInDays(date: DateTime) {
|
||||
function getDiffInDays(date) {
|
||||
const today = DateTime.now().startOf('day');
|
||||
const diffInDays = date.diff(today, 'days').days;
|
||||
const roundedDiffInDays = Math.round(diffInDays);
|
||||
|
||||
return roundedDiffInDays;
|
||||
}
|
||||
|
||||
function getFeedbackPayload(date: DateTime) {
|
||||
function getFeedbackPayload(date) {
|
||||
const diffInDays = getDiffInDays(date);
|
||||
|
||||
if (diffInDays <= -1) {
|
||||
return {
|
||||
translationEntryId: 'trialBadge.over',
|
||||
status: 'error' as const,
|
||||
status: 'error',
|
||||
over: true,
|
||||
};
|
||||
} else if (diffInDays <= 0) {
|
||||
return {
|
||||
translationEntryId: 'trialBadge.endsToday',
|
||||
status: 'warning' as const,
|
||||
status: 'warning',
|
||||
over: false,
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
translationEntryId: 'trialBadge.xDaysLeft',
|
||||
translationEntryValues: {
|
||||
remainingDays: diffInDays
|
||||
remainingDays: diffInDays,
|
||||
},
|
||||
status: 'warning' as const,
|
||||
status: 'warning',
|
||||
over: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function useTrialStatus(): UseTrialStatusReturn {
|
||||
export default function useTrialStatus() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const location = useLocation();
|
||||
const state = location.state as { checkoutCompleted: boolean };
|
||||
const state = location.state;
|
||||
const checkoutCompleted = state?.checkoutCompleted;
|
||||
const { data, loading, startPolling, stopPolling } = useQuery(GET_TRIAL_STATUS);
|
||||
const { data, loading, startPolling, stopPolling } =
|
||||
useQuery(GET_TRIAL_STATUS);
|
||||
const hasTrial = !!data?.getTrialStatus?.expireAt;
|
||||
|
||||
React.useEffect(function pollDataUntilTrialEnds() {
|
||||
if (checkoutCompleted && hasTrial) {
|
||||
startPolling(1000);
|
||||
}
|
||||
}, [checkoutCompleted, hasTrial, startPolling]);
|
||||
|
||||
React.useEffect(function stopPollingWhenTrialEnds() {
|
||||
if (checkoutCompleted && !hasTrial) {
|
||||
stopPolling();
|
||||
}
|
||||
}, [checkoutCompleted, hasTrial, stopPolling]);
|
||||
|
||||
React.useEffect(
|
||||
function pollDataUntilTrialEnds() {
|
||||
if (checkoutCompleted && hasTrial) {
|
||||
startPolling(1000);
|
||||
}
|
||||
},
|
||||
[checkoutCompleted, hasTrial, startPolling],
|
||||
);
|
||||
React.useEffect(
|
||||
function stopPollingWhenTrialEnds() {
|
||||
if (checkoutCompleted && !hasTrial) {
|
||||
stopPolling();
|
||||
}
|
||||
},
|
||||
[checkoutCompleted, hasTrial, stopPolling],
|
||||
);
|
||||
if (loading || !data.getTrialStatus) return null;
|
||||
|
||||
const expireAt = DateTime.fromMillis(Number(data.getTrialStatus.expireAt)).startOf('day');
|
||||
|
||||
const {
|
||||
translationEntryId,
|
||||
translationEntryValues,
|
||||
status,
|
||||
over,
|
||||
} = getFeedbackPayload(expireAt);
|
||||
|
||||
const expireAt = DateTime.fromMillis(
|
||||
Number(data.getTrialStatus.expireAt),
|
||||
).startOf('day');
|
||||
const { translationEntryId, translationEntryValues, status, over } =
|
||||
getFeedbackPayload(expireAt);
|
||||
return {
|
||||
message: formatMessage(translationEntryId, translationEntryValues),
|
||||
expireAt,
|
||||
over,
|
||||
status
|
||||
status,
|
||||
};
|
||||
}
|
@@ -1,16 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { IUser } from 'types';
|
||||
|
||||
import { GET_USER } from 'graphql/queries/get-user';
|
||||
|
||||
type QueryResponse = {
|
||||
getUser: IUser;
|
||||
};
|
||||
|
||||
export default function useUser(userId?: string) {
|
||||
const [getUser, { data, loading }] = useLazyQuery<QueryResponse>(GET_USER);
|
||||
|
||||
export default function useUser(userId) {
|
||||
const [getUser, { data, loading }] = useLazyQuery(GET_USER);
|
||||
React.useEffect(() => {
|
||||
if (userId) {
|
||||
getUser({
|
||||
@@ -20,7 +12,6 @@ export default function useUser(userId?: string) {
|
||||
});
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
return {
|
||||
user: data?.getUser,
|
||||
loading,
|
@@ -1,36 +1,16 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IUser } from 'types';
|
||||
|
||||
import { GET_USERS } from 'graphql/queries/get-users';
|
||||
|
||||
type Edge = {
|
||||
node: IUser;
|
||||
};
|
||||
|
||||
type QueryResponse = {
|
||||
getUsers: {
|
||||
pageInfo: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
};
|
||||
totalCount: number;
|
||||
edges: Edge[];
|
||||
};
|
||||
};
|
||||
|
||||
const getLimitAndOffset = (page: number, rowsPerPage: number) => ({
|
||||
const getLimitAndOffset = (page, rowsPerPage) => ({
|
||||
limit: rowsPerPage,
|
||||
offset: page * rowsPerPage,
|
||||
});
|
||||
|
||||
export default function useUsers(page: number, rowsPerPage: number) {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_USERS, {
|
||||
export default function useUsers(page, rowsPerPage) {
|
||||
const { data, loading } = useQuery(GET_USERS, {
|
||||
variables: getLimitAndOffset(page, rowsPerPage),
|
||||
});
|
||||
const users = data?.getUsers.edges.map(({ node }) => node) || [];
|
||||
const pageInfo = data?.getUsers.pageInfo;
|
||||
const totalCount = data?.getUsers.totalCount;
|
||||
|
||||
return {
|
||||
users,
|
||||
pageInfo,
|
@@ -1,32 +1,21 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { compare } from 'compare-versions';
|
||||
|
||||
import { HEALTHCHECK } from 'graphql/queries/healthcheck';
|
||||
import useNotifications from 'hooks/useNotifications';
|
||||
|
||||
type TVersionInfo = {
|
||||
version: string;
|
||||
newVersionCount: number;
|
||||
};
|
||||
|
||||
export default function useVersion(): TVersionInfo {
|
||||
export default function useVersion() {
|
||||
const { notifications } = useNotifications();
|
||||
const { data } = useQuery(HEALTHCHECK, { fetchPolicy: 'cache-and-network' });
|
||||
const version = data?.healthcheck.version;
|
||||
|
||||
const newVersionCount = notifications.reduce((count, notification) => {
|
||||
if (!version) return 0;
|
||||
|
||||
// an unexpectedly invalid version would throw and thus, try-catch.
|
||||
try {
|
||||
const isNewer = compare(version, notification.name, '<');
|
||||
|
||||
return isNewer ? count + 1 : count;
|
||||
} catch {
|
||||
return count;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
version,
|
||||
newVersionCount,
|
Reference in New Issue
Block a user