Skip to content

Commit

Permalink
organization config update (also cypress migration) (#234)
Browse files Browse the repository at this point in the history
* organization config update (also cypress migration)

* cypress docker fix

* upgrade cypress dockerfile

* remove outdated package

* change cypress docker tag

* test fixes, no redux mutation

* sort chat messages in redux
  • Loading branch information
aaronshiel authored May 2, 2024
1 parent 79252f3 commit e78f457
Show file tree
Hide file tree
Showing 39 changed files with 1,875 additions and 1,087 deletions.
107 changes: 107 additions & 0 deletions client/src/api-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
This software is Copyright ©️ 2020 The University of Southern California. All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational, research and non-profit purposes, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and subject to the full license file found in the root of this software deliverable. Permission to make commercial use of this software may be obtained by contacting: USC Stevens Center for Innovation University of Southern California 1150 S. Olive Street, Suite 2300, Los Angeles, CA 90115, USA Email: [email protected]
The full terms of this copyright and license should always be found in the root directory of this software deliverable as "license.txt" and if these terms are not found with this software, please contact the USC Stevens Center for the full license.
*/
import axios, { AxiosRequestConfig, AxiosResponse, Method } from "axios";

export const GATSBY_GRAPHQL_ENDPOINT =
process.env.GATSBY_GRAPHQL_ENDPOINT || "/graphql";

const REQUEST_TIMEOUT_GRAPHQL_DEFAULT = 30000;

// https://github.com/axios/axios/issues/4193#issuecomment-1158137489
interface MyAxiosRequestConfig extends Omit<AxiosRequestConfig, "headers"> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
headers?: any; // this was "any" at v0.21.1 but now broken between 0.21.4 >= 0.27.2
}

interface GQLQuery {
query: string; // the query string passed to graphql, which should be a static query
variables?: Record<string, unknown>; // variables (if any) for the static query
}

interface HttpRequestConfig {
accessToken?: string; // bearer-token http auth
axiosConfig?: MyAxiosRequestConfig; // any axios config for the request
/**
* When set, will use this prop (or array of props) to extract return data from a json response, e.g.
*
* dataPath: ["foo", "bar"]
*
* // will extract "barvalue" for the return
* { "foo": { "bar": "barvalue" } }
*/
dataPath?: string | string[];
}

async function execHttp<T>(
method: Method,
query: string,
opts?: HttpRequestConfig
): Promise<T> {
const optsEffective: HttpRequestConfig = opts || {};
const axiosConfig = opts?.axiosConfig || {};
const axiosInst = axios.create();
const result = await axiosInst.request({
url: query,
method: method,
...axiosConfig,
headers: {
...(axiosConfig.headers || {}), // if any headers passed in opts, include them
...(optsEffective && optsEffective.accessToken // if accessToken passed in opts, add auth to headers
? { Authorization: `bearer ${optsEffective.accessToken}` }
: {}),
},
});
return getDataFromAxiosResponse(result, optsEffective.dataPath || []);
}

export function throwErrorsInAxiosResponse(res: AxiosResponse): void {
if (!(res.status >= 200 && res.status <= 299)) {
throw new Error(`http request failed: ${res.data}`);
}
if (res.data.errors) {
throw new Error(`errors in response: ${JSON.stringify(res.data.errors)}`);
}
}

function getDataFromAxiosResponse(res: AxiosResponse, path: string | string[]) {
throwErrorsInAxiosResponse(res);
let data = res.data.data;
if (!data) {
throw new Error(`no data in reponse: ${JSON.stringify(res.data)}`);
}
const dataPath = Array.isArray(path)
? path
: typeof path === "string"
? [path]
: [];
dataPath.forEach((pathPart) => {
if (!data) {
throw new Error(
`unexpected response data shape for dataPath ${JSON.stringify(
dataPath
)} and request ${res.request} : ${res.data}`
);
}
data = data[pathPart];
});
return data;
}

export async function execGql<T>(
query: GQLQuery,
opts?: HttpRequestConfig
): Promise<T> {
return execHttp<T>("POST", GATSBY_GRAPHQL_ENDPOINT, {
// axiosMiddleware: applyAppTokenRefreshInterceptor,
...(opts || {}),
axiosConfig: {
timeout: REQUEST_TIMEOUT_GRAPHQL_DEFAULT, // default timeout can be overriden by passed-in config
...(opts?.axiosConfig || {}),
data: query,
},
});
}
80 changes: 28 additions & 52 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ Permission to use, copy, modify, and distribute this software and its documentat
The full terms of this copyright and license should always be found in the root directory of this software deliverable as "license.txt" and if these terms are not found with this software, please contact the USC Stevens Center for the full license.
*/
import { GATSBY_GRAPHQL_ENDPOINT, execGql } from "api-helpers";
import axios, { AxiosResponse } from "axios";
import { LS_LEFT_HOME_PAGE } from "local-constants";
import * as queries from "queries";
import {
Utterance,
Config,
Expand All @@ -21,64 +23,22 @@ import {
AuthUserData,
UtteranceGQL,
convertUtteranceGQL,
OrgCheckPermission,
} from "types-gql";
import { getLocalStorage } from "utils";

export const GATSBY_GRAPHQL_ENDPOINT =
process.env.GATSBY_GRAPHQL_ENDPOINT || "/graphql";
export async function fetchConfig(): Promise<Config> {
const gqlRes = await axios.post<GraphQLResponse<{ orgConfig: Config }>>(
GATSBY_GRAPHQL_ENDPOINT,
export async function fetchConfig(orgAccessCode: string): Promise<Config> {
return await execGql<Config>(
{
query: `
query FetchConfig {
orgConfig {
cmi5Enabled
cmi5Endpoint
cmi5Fetch
classifierLambdaEndpoint
urlGraphql
urlVideo
mentorsDefault
filterEmailMentorAddress
disclaimerTitle
disclaimerText
disclaimerDisabled
styleHeaderColor
styleHeaderTextColor
styleHeaderLogo
styleHeaderLogoUrl
displayGuestPrompt
displaySurveyPopupCondition
defaultVirtualBackground
postSurveyLink
postSurveyTimer
minTopicQuestionSize
postSurveyUserIdEnabled
postSurveyReferrerEnabled
surveyButtonInDisclaimer
guestPromptInputType
guestPromptTitle
guestPromptText
}
}
`,
query: `${queries.getOrgConfig}`,
variables: {
orgAccessCode: orgAccessCode,
},
},
{
dataPath: ["orgConfig"],
}
);
if (gqlRes.status !== 200) {
throw new Error(`orgConfig load failed: ${gqlRes.statusText}}`);
}
if (gqlRes.data.errors) {
throw new Error(
`errors reponse to orgConfig query: ${JSON.stringify(gqlRes.data.errors)}`
);
}
if (!gqlRes.data.data) {
throw new Error(
`no data in non-error reponse: ${JSON.stringify(gqlRes.data)}`
);
}
return gqlRes.data.data.orgConfig;
}

export function getUtterance(
Expand Down Expand Up @@ -507,3 +467,19 @@ export async function refreshAccessToken(): Promise<AuthUserData> {
const accessTokenData = gqlRes.data.data.refreshAccessToken;
return accessTokenData;
}

export async function fetchOrgPerm(
orgAccessCode?: string
): Promise<OrgCheckPermission> {
return await execGql<OrgCheckPermission>(
{
query: `${queries.checkOrgPermission}`,
variables: {
orgAccessCode: orgAccessCode,
},
},
{
dataPath: ["orgCheckPermission"],
}
);
}
2 changes: 1 addition & 1 deletion client/src/components/chat/chat-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function ChatItem(props: {
const chatLink = hrefToChatLink(props?.node?.url || "", message);
return chatLink.type === LINK_TYPE_ASK ? (
<a
href="#"
// href="#"
rel="noreferrer"
onClick={() =>
onAskLinkClicked(chatLink.question, MentorQuestionSource.CHAT_LINK)
Expand Down
26 changes: 1 addition & 25 deletions client/src/components/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,8 @@ function Chat(props: {
}, {});
});

const lastQuestionCounter = useSelector<State, number>(
(s) => s.chat.lastQuestionCounter || s.questionsAsked.length + 1
);

const chatData = useSelector<State, ChatData>((s) => s.chat);

function isQuestionsAnswersVisible(
questionId: string,
isIntro: boolean
Expand Down Expand Up @@ -138,27 +135,6 @@ function Chat(props: {
Boolean(userPref === ItemVisibilityPrefs.VISIBLE);
}

if (mentorType !== "CHAT") {
// get last answers
const lastAnswers = chatData.messages.filter((m) => {
return m.questionCounter === lastQuestionCounter && !m.isUser;
});

// sort last answers by timestampAnswered
const answersSorted = lastAnswers.sort((a, b) =>
String(a.timestampAnswered).localeCompare(String(b.timestampAnswered))
);

// remove unsorted answers
chatData.messages.splice(
chatData.messages.length - Object.keys(answersSorted).length,
chatData.messages.length
);

// add sorted answers to chat
chatData.messages.push(...answersSorted);
}

return (
<div
data-cy="history-chat"
Expand Down
Loading

0 comments on commit e78f457

Please sign in to comment.