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",
"lockfileVersion": 1,
"requires": true,
......
// The Less file used to compiled as App.css
.eclipseFdn-membership-webform {
.error {
color: red;
......
This source diff could not be displayed because it is too large. You can view the blob instead.
/* Compiled from App.less */
.eclipseFdn-membership-webform {
/* Stepper */
}
......
import React from 'react';
import React, { useState } from 'react';
import './App.css';
import AppFooter from './components/layout/AppFooter';
import AppHeader from './components/layout/AppHeader';
import MembershipProvider from "./Context/MembershipProvider";
import FormWrapper from "./components/FormPreprocess/FormWrapper";
import MembershipContext from "./Context/MembershipContext";
const App = () => {
const [currentUser, setCurrentUser] = useState(null);
const [currentFormId, setCurrentFormId] = useState("");
return (
<div className="App">
<AppHeader />
<MembershipProvider>
<MembershipContext.Provider value={{
currentUser,
setCurrentUser: (val) => setCurrentUser(val),
currentFormId,
setCurrentFormId: (val) => setCurrentFormId(val)
}}>
<FormWrapper />
</MembershipProvider>
</MembershipContext.Provider>
<AppFooter />
</div>
);
......
// 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_form = 'http://localhost:8090/form';
......
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({
isExistingMember: false,
setIsExistingMember: () => {},
currentUser: {},
setCurrentUser: () => {},
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) {
//== 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 -
* Existing Organization data, fetched from server
......@@ -49,27 +61,25 @@ export function matchCompanyFields(existingOrganizationData) {
return {
// Step1: company Info
organization: {
id: existingOrganizationData?.id || '',
legalName: {
value: existingOrganizationData?.legal_name || '',
label: existingOrganizationData?.legal_name || '',
address: existingOrganizationData?.address || '',
twitterHandle: existingOrganizationData?.twitter_handle || ''
id: existingOrganizationData?.id || '',
legalName: {
value: existingOrganizationData?.legal_name || '',
label: existingOrganizationData?.legal_name || '',
address: existingOrganizationData?.address || '',
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: {
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
} || '',
postalCode: existingOrganizationData?.address.postal_code || '',
},
twitterHandle: existingOrganizationData?.twitter_handle || '',
}
postalCode: existingOrganizationData?.address.postal_code || '',
},
twitterHandle: existingOrganizationData?.twitter_handle || '',
}
}
......@@ -99,33 +109,32 @@ export function matchContactFields(existingContactData) {
let existingAccoutingContact = existingContactData.find(el => el.type === contact_type.ACCOUNTING)
return {
companyRepresentative: {
representative: {
id: existingCompanyContact?.id || '',
firstName: existingCompanyContact?.first_name || '',
lastName: existingCompanyContact?.last_name || '',
jobtitle: existingCompanyContact?.job_title || '',
email: existingCompanyContact?.email || ''
},
marketingRepresentative: {
id: existingMarketingContact?.id || '',
firstName: existingMarketingContact?.first_name || '',
lastName: existingMarketingContact?.last_name || '',
jobtitle: existingMarketingContact?.job_title || '',
email: existingMarketingContact?.email || '',
sameAsCompany: checkSameContact(existingCompanyContact, existingMarketingContact)
},
accounting: {
id: existingAccoutingContact?.id || '',
firstName: existingAccoutingContact?.first_name || '',
lastName: existingAccoutingContact?.last_name || '',
jobtitle: existingAccoutingContact?.job_title || '',
email: existingAccoutingContact?.email || '',
sameAsCompany: checkSameContact(existingCompanyContact, existingAccoutingContact)
}
company: {
id: existingCompanyContact?.id || '',
firstName: existingCompanyContact?.first_name || '',
lastName: existingCompanyContact?.last_name || '',
jobtitle: existingCompanyContact?.job_title || '',
email: existingCompanyContact?.email || ''
},
marketing: {
id: existingMarketingContact?.id || '',
firstName: existingMarketingContact?.first_name || '',
lastName: existingMarketingContact?.last_name || '',
jobtitle: existingMarketingContact?.job_title || '',
email: existingMarketingContact?.email || '',
sameAsCompany: checkSameContact(existingCompanyContact, existingMarketingContact)
},
accounting: {
id: existingAccoutingContact?.id || '',
firstName: existingAccoutingContact?.first_name || '',
lastName: existingAccoutingContact?.last_name || '',
jobtitle: existingAccoutingContact?.job_title || '',
email: existingAccoutingContact?.email || '',
sameAsCompany: checkSameContact(existingCompanyContact, existingAccoutingContact)
}
}
}
......@@ -288,9 +297,9 @@ export async function executeSendDataByStep(step, formData, formId, userId) {
switch(step) {
case 0:
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.companyRepresentative.marketingRepresentative, 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.company, contact_type.COMPANY, formId))
sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.representative.marketing, contact_type.MARKETING, formId))
sendData(formId, end_point.contacts, matchContactFieldsToBackend(formData.representative.accounting, contact_type.ACCOUNTING, formId))
break;
case 1:
......@@ -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}
* @param dataBody -
* 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) {
......@@ -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
* @param defaultBehaviour -
* 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) {
......
import React, { useContext } from 'react';
import React from 'react';
import CustomSelectWrapper from '../Inputs/CustomSelect/CustomSelectWrapper';
import DefaultSelect from '../Inputs/CustomSelect/DefaultSelect';
import CustomAsyncSelect from '../Inputs/CustomSelect/CustomAsyncSelect';
import MembershipContext from '../../../Context/MembershipContext';
import Input from '../Inputs/Input';
import { formField } from '../formModels/formFieldModel';
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 { isExistingMember } = useContext(MembershipContext);
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 }));
return (
......@@ -19,7 +23,6 @@ const Company = () => {
name={organizationName.name}
ariaLabel={organizationName.name}
srcData={companies}
isExistingMember={isExistingMember}
renderComponent={CustomAsyncSelect}
/>
<div className="row">
......
......@@ -6,29 +6,45 @@ import Contacts from './Contacts';
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';
/**
* - 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 {currentFormId} = useContext(MembershipContext);
const formValues = otherProps.parentState.formik.values;
const { currentFormId } = useContext(MembershipContext); // current chosen form id
const formValues = otherProps.parentState.formik.values; // current form values
const { setFieldValue } = otherProps.parentState.formik;
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(() => {
// 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_suffix_local = '';
// If running on localhost:3000
if ( getCurrentMode() === MODE_REACT_ONLY ) {
url_prefix_local = 'membership_data';
url_suffix_local = '.json';
url_prefix_local = 'membership_data'; // --> public/membership_data/
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) {
url_prefix_local = api_prefix_form;
}
// If the current form exsits, and it is not creating a new form
if (currentFormId && currentFormId !== newForm_tempId) {
// Using promise pool, because in first step, need to get company data, and contacts data
let pool = [
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 })
]
];
Promise.all(pool)
.then((res) =>
......@@ -36,24 +52,28 @@ const CompanyInformation = ({ formField, ...otherProps }) => {
)
.then(([organizations, contacts]) => {
// Matching the field data
if (organizations[0]) {
let tempOrg = matchCompanyFields(organizations[0])
otherProps.parentState.formik.setFieldValue('organization', tempOrg.organization)
if (organizations[0]) { // the organization data returned is always an array of one object, that is why using [0]
// Call the the function to map the retrived organization backend data to fit frontend
let tempOrg = matchCompanyFields(organizations[0]);
console.log(tempOrg)
// Call the setFieldValue of Formik, to set organization field with the mapped data, if nested, it will automatically map the properties and values
setFieldValue('organization', tempOrg);
}
if(contacts.length) {
let tempContacts = matchContactFields(contacts)
// Prefill Data
otherProps.parentState.formik.setFieldValue('companyRepresentative', tempContacts.companyRepresentative)
// Call the the function to map the retrived contacts (company representative, marketing rep, accounting rep) backend data to fit frontend
let tempContacts = matchContactFields(contacts);
// Prefill Data --> Call the setFieldValue of Formik, to set representative field with the mapped data, if nested, it will automatically map the properties and values
setFieldValue('representative', tempContacts);
}
setLoading(false);
})
}
else {
}
else {
setLoading(false);
}
// eslint-disable-next-line
}, [])
}
}, [currentFormId, setFieldValue])
// If it is in loading status, only return a loading spinning
if (loading) {
return <Loading />
}
......
......@@ -2,16 +2,32 @@ import React from 'react';
import Input from '../Inputs/Input';
import CustomCheckbox from '../Inputs/CustomCheckbox';
/**
* - Render three representatives inputs, include checkbox
* - Props:
* - formValues: current form values; passed from CompanyInformation component;
* - formField: the form field in formModels/formFieldModel.js
* **/
const Contacts = ({formValues, formField}) => {
const mktSame = formValues.companyRepresentative.marketingRepresentative.sameAsCompany;
const accSame = formValues.companyRepresentative.accounting.sameAsCompany;
const { companyRepresentative, marketingRepresentative, accounting } = formField;
const mktSame = formValues.representative.marketing.sameAsCompany; // the boolean form value of "is marketing Rep. the same as company Rep.?"
const accSame = formValues.representative.accounting.sameAsCompany; // the boolean form value of "is accounting Rep. the same as company Rep.?"
const { company, marketing, accounting } = formField;
const generateContacts = (representatives, prefix, disableInput) => {
// Generate Representatives Inputs components
/**
* @param representativeFields -
* company, marketing or accounting
* @param prefix -
* simply to add it in the key prop, so that each component has a unique key
* @param disableInput -
* if marketing / accounting is the same as company Rep., mark the input disabled and just used the same values from company Rep.
* **/
const generateContacts = (representativeFields, prefix, disableInput) => {
return (
<>
{ representatives.map((el, index) =>
{ representativeFields.map((el, index) =>
<div key={prefix + index} className="col-md-12">
<Input name={el.name} labelName={el.label} ariaLabel={prefix + el.label} placeholder={el.placeholder} disableInput={disableInput} />
</div>
......@@ -26,20 +42,20 @@ const Contacts = ({formValues, formField}) => {
<p>Please indicate the primary point of contact between your organization and the Eclipse Foundation. As per the Eclipse Bylaws, the Member Representative shall represent your organization in the General Assembly, have the right to cast any votes on behalf of your organization, and shall have the authority to update information provided to Eclipse Foundation.</p>
<p>All formal communications from the Eclipse Foundation will be sent to the Member Representative.</p>
<div className="row">