Unverified Commit 041d68be authored by Martin Lowe's avatar Martin Lowe 🇨🇦 Committed by GitHub
Browse files

Reupload of Yi doc fix (#53)



* Add comments on components and functions, and renamed a variable to make it make sense
Signed-off-by: Yi Liu's avatarYi Liu <yi.liu@eclipse-foundation.org>

More comments
Signed-off-by: Yi Liu's avatarYi Liu <yi.liu@eclipse-foundation.org>

more comments in the rest of the components
Signed-off-by: Yi Liu's avatarYi Liu <yi.liu@eclipse-foundation.org>

More comments, and improve code practice
Signed-off-by: Yi Liu's avatarYi Liu <yi.liu@eclipse-foundation.org>

* Update to fix missing dependencies in context
Signed-off-by: Martin Lowe's avatarMartin Lowe <martin.lowe@eclipse-foundation.org>

* Fix package lock versions (regen in 6.x)
Signed-off-by: Martin Lowe's avatarMartin Lowe <martin.lowe@eclipse-foundation.org>

* Update to publish dir to point at nested folder
Signed-off-by: Martin Lowe's avatarMartin Lowe <martin.lowe@eclipse-foundation.org>
Co-authored-by: Yi Liu's avatarYi Liu <yi.liu@eclipse-foundation.org>
Co-authored-by: Christopher Guindon's avatarChristopher Guindon <chris.guindon@eclipse-foundation.org>
parent f2a93489
.eclipseFdn-membership-webform {
.error {
color: red;
}
.orange-star {
color: #f78700;
}
.display-center {
display: flex;
align-items: center;
justify-content: center;
}
.form-control {
border-radius: 5px;
box-shadow: none;
height: 36px;
}
label {
font-weight: unset;
}
.form-border-error {
border-color: red;
}
.btn {
margin: 0 10px;
border-radius: 5px;
}
.button-container {
display: flex;
align-items: center;
justify-content: center;
}
label#effective-date-label {
display: none;
}
select.form-control {
width: 50%;
}
.align-center {
max-width: 80%;
margin-left: auto;
margin-right: auto;
}
input[type=checkbox] {
display: inline-block;
width: 30px;
height: 30px;
border: 1px solid hsl(0, 0%, 80%);
border-radius: 5px;
margin-top: 0;
margin-right: 15px;
}
input[type=checkbox]:checked {
background-color: orange;
}
.verical-center {
display: flex;
align-items: center;
}
input[type=checkbox]:focus {
outline: none !important;
}
.preview-field {
padding: 5px;
border: 1px solid hsl(0, 0%, 80%);
border-radius: 5px;
background: #fff;
min-height: 32px;
}
/* Stepper */
.stepper {
display: flex;
flex-wrap: wrap;
margin-top: 0;
margin-left: auto;
margin-right: auto;
margin-bottom: 50px;
}
.step {
width: calc(100% / 3);
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
padding-top: 30px;
}
.step-span {
width: 50px;
height: 50px;
background: #f7941e;
}
.step-span-index {
font-size: xx-large;
font-weight: 600;
padding-left: 15px;
}
.step-title {
padding: 10px;
font-weight: 600;
}
.step-title-container {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
min-height: 60px;
min-width: 100px;
background-color: #e2e2e2;
position: relative;
top: 15px;
left: 15px;
}
.step-title-container-active:extend(.eclipseFdn-membership-webform .step-title-container) {
// &:extend(.step-title-container);
background: #404040 50% no-repeat;
color: white;
}
// Mobile First
@media (min-width: 992px) {
.step {
width: calc(100% / 6);
padding-top: 10px;
}
}
}
\ No newline at end of file
{ {
"name": "my-keycloak-app", "name": "eclipsefdn-members",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
......
// The Less file used to compiled as App.css
.eclipseFdn-membership-webform { .eclipseFdn-membership-webform {
.error { .error {
color: red; color: red;
......
This diff is collapsed.
/* Compiled from App.less */
.eclipseFdn-membership-webform { .eclipseFdn-membership-webform {
/* Stepper */ /* Stepper */
} }
......
import React from 'react'; import React, { useState } from 'react';
import './App.css'; import './App.css';
import AppFooter from './components/layout/AppFooter'; import AppFooter from './components/layout/AppFooter';
import AppHeader from './components/layout/AppHeader'; import AppHeader from './components/layout/AppHeader';
import MembershipProvider from "./Context/MembershipProvider";
import FormWrapper from "./components/FormPreprocess/FormWrapper"; import FormWrapper from "./components/FormPreprocess/FormWrapper";
import MembershipContext from "./Context/MembershipContext";
const App = () => { const App = () => {
const [currentUser, setCurrentUser] = useState(null);
const [currentFormId, setCurrentFormId] = useState("");
return ( return (
<div className="App"> <div className="App">
<AppHeader /> <AppHeader />
<MembershipProvider> <MembershipContext.Provider value={{
currentUser,
setCurrentUser: (val) => setCurrentUser(val),
currentFormId,
setCurrentFormId: (val) => setCurrentFormId(val)
}}>
<FormWrapper /> <FormWrapper />
</MembershipProvider> </MembershipContext.Provider>
<AppFooter /> <AppFooter />
</div> </div>
); );
......
// list all constants here // list all constants here
// The purpose of this file is try to avoid using strings directly everywhere, just hope to use consistant variables for strings
export const api_prefix = 'http://localhost:8090'; export const api_prefix = 'http://localhost:8090';
export const api_prefix_form = 'http://localhost:8090/form'; export const api_prefix_form = 'http://localhost:8090/form';
......
import React from "react"; import React from "react";
/**
* User and form Id context shared among the whole App
*
* For more about context, please refer to: https://reactjs.org/docs/context.html
*
* It is simliar to state, but you can export and import anywhere, no need to pass all the way down to the child component
* **/
const MembershipContext = React.createContext({ const MembershipContext = React.createContext({
isExistingMember: false,
setIsExistingMember: () => {},
currentUser: {}, currentUser: {},
setCurrentUser: () => {}, setCurrentUser: () => {},
currentFormId: "", currentFormId: "",
......
import React, { useState } from "react";
import MembershipContext from "./MembershipContext";
const MembershipProvider = ({ children }) => {
const [isExistingMember, setIsExistingMember] = useState(false)
const [currentUser, setCurrentUser] = useState(null)
const [currentFormId, setCurrentFormId] = useState("")
// useEffect(() => {
// If has login data, can put here to set if is existing member
// })
// useEffect(() => {
// fetch('membership_data/fake_user.json',{
// headers : {
// 'Content-Type': 'application/json',
// 'Accept': 'application/json'
// }})
// .then(resp => resp.json())
// .then(data => {
// setCurrentUser(data);
// })
// }, [])
return (
<MembershipContext.Provider value={{
isExistingMember,
setIsExistingMember: (val) => setIsExistingMember(val),
currentUser,
setCurrentUser: (val) => setCurrentUser(val),
currentFormId,
setCurrentFormId: (val) => setCurrentFormId(val)
}}>
{children}
</MembershipContext.Provider>
)
}
export default MembershipProvider
\ No newline at end of file
...@@ -41,6 +41,18 @@ export function assignContactData(currentContact, companyContact) { ...@@ -41,6 +41,18 @@ export function assignContactData(currentContact, companyContact) {
//== Transform data from backend to match my form model //== Transform data from backend to match my form model
/**
* Notes:
* The match data functions look repeated on the properties, because of the naming convention is somehow different in JS and the data object retrieved from backend. JS uses camelCase.
* Another reason I am making complicated `legalName` and `country` match, is due to the library React-Select. It needs to use the `{label: foo, value: foo}` format to be able shown on the select input fields.
* Please refer to: https://github.com/formium/formik/discussions/2954
* and https://github.com/JedWatson/react-select/issues/3761
* and https://github.com/JedWatson/react-select/issues/4321
*
* https://github.com/JedWatson/react-select/issues/3761 shows how you can use without mapping as a label, but I wanted to use the label to be shown in the preview, thus, still used { label: value: } format.
*
* **/
/** /**
* @param existingOrganizationData - * @param existingOrganizationData -
* Existing Organization data, fetched from server * Existing Organization data, fetched from server
...@@ -49,27 +61,25 @@ export function matchCompanyFields(existingOrganizationData) { ...@@ -49,27 +61,25 @@ export function matchCompanyFields(existingOrganizationData) {
return { return {
// Step1: company Info // Step1: company Info
organization: { id: existingOrganizationData?.id || '',
id: existingOrganizationData?.id || '', legalName: {
legalName: { value: existingOrganizationData?.legal_name || '',
value: existingOrganizationData?.legal_name || '', label: existingOrganizationData?.legal_name || '',
label: existingOrganizationData?.legal_name || '', address: existingOrganizationData?.address || '',
address: existingOrganizationData?.address || '', twitterHandle: existingOrganizationData?.twitter_handle || ''
twitterHandle: existingOrganizationData?.twitter_handle || '' } || '',
address: {
id: existingOrganizationData?.address.id || '',
street: existingOrganizationData?.address.street || '',
city: existingOrganizationData?.address.city || '',
provinceOrState: existingOrganizationData?.address.province_state || '',
country: {
label: existingOrganizationData?.address.country,
value: existingOrganizationData?.address.country
} || '', } || '',
address: { postalCode: existingOrganizationData?.address.postal_code || '',
id: existingOrganizationData?.address.id || '', },
street: existingOrganizationData?.address.street || '', twitterHandle: existingOrganizationData?.twitter_handle || '',
city: existingOrganizationData?.address.city || '',
provinceOrState: existingOrganizationData?.address.province_state || '',
country: {
label: existingOrganizationData?.address.country,
value: existingOrganizationData?.address.country
} || '',
postalCode: existingOrganizationData?.address.postal_code || '',
},
twitterHandle: existingOrganizationData?.twitter_handle || '',
}
} }
} }
...@@ -99,33 +109,32 @@ export function matchContactFields(existingContactData) { ...@@ -99,33 +109,32 @@ export function matchContactFields(existingContactData) {
let existingAccoutingContact = existingContactData.find(el => el.type === contact_type.ACCOUNTING) let existingAccoutingContact = existingContactData.find(el => el.type === contact_type.ACCOUNTING)
return { return {
companyRepresentative: { company: {
representative: { id: existingCompanyContact?.id || '',
id: existingCompanyContact?.id || '', firstName: existingCompanyContact?.first_name || '',
firstName: existingCompanyContact?.first_name || '', lastName: existingCompanyContact?.last_name || '',
lastName: existingCompanyContact?.last_name || '', jobtitle: existingCompanyContact?.job_title || '',
jobtitle: existingCompanyContact?.job_title || '', email: existingCompanyContact?.email || ''
email: existingCompanyContact?.email || '' },
},
marketing: {
marketingRepresentative: { id: existingMarketingContact?.id || '',
id: existingMarketingContact?.id || '', firstName: existingMarketingContact?.first_name || '',
firstName: existingMarketingContact?.first_name || '', lastName: existingMarketingContact?.last_name || '',
lastName: existingMarketingContact?.last_name || '', jobtitle: existingMarketingContact?.job_title || '',
jobtitle: existingMarketingContact?.job_title || '', email: existingMarketingContact?.email || '',
email: existingMarketingContact?.email || '', sameAsCompany: checkSameContact(existingCompanyContact, existingMarketingContact)
sameAsCompany: checkSameContact(existingCompanyContact, existingMarketingContact) },
},
accounting: {
accounting: { id: existingAccoutingContact?.id || '',
id: existingAccoutingContact?.id || '', firstName: existingAccoutingContact?.first_name || '',
firstName: existingAccoutingContact?.first_name || '', lastName: existingAccoutingContact?.last_name || '',
lastName: existingAccoutingContact?.last_name || '', jobtitle: existingAccoutingContact?.job_title || '',
jobtitle: existingAccoutingContact?.job_title || '', email: existingAccoutingContact?.email || '',
email: existingAccoutingContact?.email || '', sameAsCompany: checkSameContact(existingCompanyContact, existingAccoutingContact)
sameAsCompany: checkSameContact(existingCompanyContact, existingAccoutingContact)
}
} }
} }
} }
...@@ -288,9 +297,9 @@ export async function executeSendDataByStep(step, formData, formId, userId) { ...@@ -288,9 +297,9 @@ export async function executeSendDataByStep(step, formData, formId, userId) {
switch(step) { switch(step) {
case 0: case 0:
sendData(formId, end_point.organizations, matchCompanyFieldsToBackend(formData.organization, formId)) sendData(formId, end_point.organizations, matchCompanyFieldsToBackend(formData.organization, formId))
sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.companyRepresentative.representative, contact_type.COMPANY, formId)) sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.representative.company, contact_type.COMPANY, formId))
sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.companyRepresentative.marketingRepresentative, contact_type.MARKETING, formId)) sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.representative.marketing, contact_type.MARKETING, formId))
sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.companyRepresentative.accounting, contact_type.ACCOUNTING, formId)) sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.representative.accounting, contact_type.ACCOUNTING, formId))
break; break;
case 1: case 1:
...@@ -367,6 +376,8 @@ function callSendData(formId, endpoint='', method, dataBody, entityId='') { ...@@ -367,6 +376,8 @@ function callSendData(formId, endpoint='', method, dataBody, entityId='') {
* /form/{id}, /form/{id}/organizations/{id}, /form/{id}/contacts/{id}, /form/{id}/working_groups/{id} * /form/{id}, /form/{id}/organizations/{id}, /form/{id}/contacts/{id}, /form/{id}/working_groups/{id}
* @param dataBody - * @param dataBody -
* The data body passed to server, normally is the filled form data to be saved * The data body passed to server, normally is the filled form data to be saved
*
* If no data.id, means it's a new data entry, we should use POST. otherwise, use PUT
* **/ * **/
export function sendData(formId, endpoint, dataBody) { export function sendData(formId, endpoint, dataBody) {
...@@ -453,6 +464,11 @@ export function deleteData(formId, endpoint, entityId, callback, index) { ...@@ -453,6 +464,11 @@ export function deleteData(formId, endpoint, entityId, callback, index) {
* User Id fetched from the server when sign in, sotored in membership context, used for calling APIs * User Id fetched from the server when sign in, sotored in membership context, used for calling APIs
* @param defaultBehaviour - * @param defaultBehaviour -
* Go to the next step and add this step to complete set, passed from FormikStepper Component * Go to the next step and add this step to complete set, passed from FormikStepper Component
*
* The logic:
* - POST a new form and returned the new form Id
* - Store the returned new form Id in my FormId Context
* - Send the API calls to organizations and contacts
* **/ * **/
export async function handleNewForm(setCurrentFormId, formData, userId, defaultBehaviour) { export async function handleNewForm(setCurrentFormId, formData, userId, defaultBehaviour) {
......
import React, { useContext } from 'react'; import React from 'react';
import CustomSelectWrapper from '../Inputs/CustomSelect/CustomSelectWrapper'; import CustomSelectWrapper from '../Inputs/CustomSelect/CustomSelectWrapper';
import DefaultSelect from '../Inputs/CustomSelect/DefaultSelect'; import DefaultSelect from '../Inputs/CustomSelect/DefaultSelect';
import CustomAsyncSelect from '../Inputs/CustomSelect/CustomAsyncSelect'; import CustomAsyncSelect from '../Inputs/CustomSelect/CustomAsyncSelect';
import MembershipContext from '../../../Context/MembershipContext';
import Input from '../Inputs/Input'; import Input from '../Inputs/Input';
import { formField } from '../formModels/formFieldModel'; import { formField } from '../formModels/formFieldModel';
import { companies } from '../../../Constants/Constants'; import { companies } from '../../../Constants/Constants';
/**
* - Render Oraganization selector (used React-Select)
* - Render Organization twitter, and address inputs, including Country selector (used React-Select and country-list library of updated correct country list names)
* **/
const Company = () => { const Company = () => {
const { isExistingMember } = useContext(MembershipContext);
const { organizationName, organizationTwitter, organizationAddress } = formField; const { organizationName, organizationTwitter, organizationAddress } = formField;
// get country list library and map as option pass to the React-Select
const countryList = require('country-list').getNames().map(item => ({ label: item, value: item })); const countryList = require('country-list').getNames().map(item => ({ label: item, value: item }));
return ( return (
...@@ -19,7 +23,6 @@ const Company = () => { ...@@ -19,7 +23,6 @@ const Company = () => {
name={organizationName.name} name={organizationName.name}
ariaLabel={organizationName.name} ariaLabel={organizationName.name}
srcData={companies} srcData={companies}
isExistingMember={isExistingMember}
renderComponent={CustomAsyncSelect} renderComponent={CustomAsyncSelect}
/> />
<div className="row"> <div className="row">
......
...@@ -6,29 +6,45 @@ import Contacts from './Contacts'; ...@@ -6,29 +6,45 @@ import Contacts from './Contacts';
import Loading from '../../Loading/Loading'; import Loading from '../../Loading/Loading';
import { end_point, api_prefix_form, FETCH_HEADER, newForm_tempId, getCurrentMode, MODE_REACT_ONLY, MODE_REACT_API } from '../../../Constants/Constants'; import { end_point, api_prefix_form, FETCH_HEADER, newForm_tempId, getCurrentMode, MODE_REACT_ONLY, MODE_REACT_API } from '../../../Constants/Constants';
/**
* - Wrapper for Contacts and Company components, with fetch and prefill data operation
*
* - Props:
* - otherProps: any other props passing down from MultiStepForm and FormikStepper components, including formik props of formik library (such as "formik.values", "formik.setFieldValue");
*
* - formField: the form field in formModels/formFieldModel.js
* **/
const CompanyInformation = ({ formField, ...otherProps }) => { const CompanyInformation = ({ formField, ...otherProps }) => {
const {currentFormId} = useContext(MembershipContext); const { currentFormId } = useContext(MembershipContext); // current chosen form id
const formValues = otherProps.parentState.formik.values; const formValues = otherProps.parentState.formik.values; // current form values
const { setFieldValue } = otherProps.parentState.formik;
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
// Fetch data only once and prefill data, behaves as componentDidMount // Fetch data only once and prefill data, as long as currentFormId and setFieldValue Function does not change, will not cause re-render again
useEffect(() => { useEffect(() => {
// Once we have API set up ready, we don't need the fake data anymore, and can remove these pre-process. it is mainly for if running the application only react without server.
let url_prefix_local; let url_prefix_local;
let url_suffix_local = ''; let url_suffix_local = '';
// If running on localhost:3000
if ( getCurrentMode() === MODE_REACT_ONLY ) { if ( getCurrentMode() === MODE_REACT_ONLY ) {
url_prefix_local = 'membership_data'; url_prefix_local = 'membership_data'; // --> public/membership_data/
url_suffix_local = '.json'; url_suffix_local = '.json'; // --> it is the fake json file
} }
// If running on localhost:8090 or any other not on localhost:3000
// Once we have the API ready running on production, will use the correct domain name rather than localhost:8090
if (getCurrentMode() === MODE_REACT_API) { if (getCurrentMode() === MODE_REACT_API) {
url_prefix_local = api_prefix_form; url_prefix_local = api_prefix_form;
} }
// If the current form exsits, and it is not creating a new form
if (currentFormId && currentFormId !== newForm_tempId) { if (currentFormId && currentFormId !== newForm_tempId) {
// Using promise pool, because in first step, need to get company data, and contacts data
let pool = [ let pool = [
fetch(url_prefix_local + `/${currentFormId}/` + end_point.organizations + url_suffix_local, { headers : FETCH_HEADER }), fetch(url_prefix_local + `/${currentFormId}/` + end_point.organizations + url_suffix_local, { headers : FETCH_HEADER }),
fetch(url_prefix_local + `/${currentFormId}/` + end_point.contacts + url_suffix_local, { headers : FETCH_HEADER }) fetch(url_prefix_local + `/${currentFormId}/` + end_point.contacts + url_suffix_local, { headers : FETCH_HEADER })
] ];
Promise.all(pool) Promise.all(pool)
.then((res) => .then((res) =>
...@@ -36,24 +52,28 @@ const CompanyInformation = ({ formField, ...otherProps }) => { ...@@ -36,24 +52,28 @@ const CompanyInformation = ({ formField, ...otherProps }) => {
) )
.then(([organizations, contacts]) => { .then(([organizations, contacts]) => {
// Matching the field data