Yup validation: Require all or none fields of a language - forms

We have quite a large form with multilang inputs - a few fields:
name_german
name_french
name_italian
description_german
description_french
description_italian
... many more
In case that one german field is filled, all other german fields should be required. Same goes with the other languages. It should be possible to have both german and french fields filled, but the italian ones can be empty.
Somehow I can't get it working. This is what I tried:
Yup.object().shape({
name_german: Yup.string().test(
'requireAllIfOneIsFilled',
'formvalidation.required.message',
function () {
return multiLanguageTest(this.parent, 'german');
},
),
... // same for other fields
});
Then do the test like this:
const multiLanguageTest = (formValues, language: 'german' | 'french' | 'italian'): boolean => {
const errorCount = dependentFields.reduce((errorCount, rawFieldName) => {
const postFixedField = `${rawFieldName}_${language}`;
if (!formValues[postFixedField]) {
return errorCount + 1;
}
return errorCount;
}, 0);
return errorCount === 0;
};
This gives me quite an undebuggable behavior. Am I using the concept of .test wrong?

It is possible to validate in this manner using the context option passed in when calling .validate.
A simple but illustrative example of the context option - passing a require_name key (with truthy value) to the .validate call will change the schema to be .required():
const NameSchema = yup.string().when(
'$require_name',
(value, schema) => value ? schema.required() : schema
);
const name = await NameSchema.validate('joe', {
context: {
require_name: true,
}
});
If we can pass in the required fields to the schema during validation, we can use the following schema for your validation case:
const AllOrNoneSchema = yup.object({
name_german: yup.string()
.when('$name_german', (value, schema) => value ? schema.required() : schema),
description_german: yup.string()
.when('$description_german', (value, schema) => value ? schema.required() : schema),
date_german: yup.string()
.when('$date_german', (value, schema) => value ? schema.required() : schema),
// ...other_languages
});
In your case, we must pass in a required context key for each language if one or more of the fields is filled out. i.e. If one German field has been filled out, we want to pass an object with all ${something}_german keys whose values are true:
{
name_german: true,
description_german: true,
...
}
To do this, we can create a helper function that takes your form object and returns a record with boolean values as in the example above:
const createValidationContext = (form) => {
const entries = Object.entries(form);
const requiredLanguages = entries.reduce((acc, [key, value]) => {
return !value.length ? acc : [...acc, key.split('_')[1]];
}, []);
return Object.fromEntries(
entries.map(([key, value]) => [key, requiredLanguages.includes(key.split('_')[1])])
)
};
Putting it all together, we have the following working solution:
const yup = require("yup");
const AllOrNoneSchema = yup.object({
name_german: yup.string()
.when('$name_german', (value, schema) => value ? schema.required() : schema),
description_german: yup.string()
.when('$description_german', (value, schema) => value ? schema.required() : schema),
date_german: yup.string()
.when('$date_german', (value, schema) => value ? schema.required() : schema),
});
const createValidationContext = (form) => {
const entries = Object.entries(form);
const requiredLanguages = entries.reduce((acc, [key, value]) => {
return !value.length ? acc : [...acc, key.split('_')[1]];
}, []);
return Object.fromEntries(
entries.map(([key, value]) => [key, requiredLanguages.includes(key.split('_')[1])])
)
};
const form = {
description_german: "nein",
date_german: "juli",
name_german: "nena",
}
const formContext = createValidationContext(form);
console.log(formContext); // { description_german: true, date_german: true, name_german: true }
const validatedForm = await AllOrNoneSchema.validate(form, { context: formContext });
console.log(validatedForm); // { description_german: "nein", date_german: "juli", name_german: "nena" }
A live example of this solution, with multiple languages, can be seen an tested on RunKit, here: https://runkit.com/joematune/6138ca762dc6340009691d8a
For more information on Yup's context api, check out their docs here: https://github.com/jquense/yup#mixedwhenkeys-string--arraystring-builder-object--value-schema-schema-schema

Related

React-Query useQueries hook to run useInfiniteQuery hooks in parallel

I am new to React-Query, but I have not been able to find an example to the following question:
Is it possible to use useInfiniteQuery within useQueries?
I can see from the parallel query documentation on GitHub, that it's fairly easy to set-up a map of normal queries.
The example provided:
function App({ users }) {
const userQueries = useQueries({
queries: users.map(user => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
})
})
}
If I have an infinite query like the following, how would I be able to provide the individual query options, specifically the page parameter?:
const ids: string[] = ['a', 'b', 'c'];
const useGetDetailsById = () => {
return useInfiniteQuery<GetDetailsByIdResponse, AxiosError>(
['getDetailsById', id],
async ({ pageParam = '' }) => {
const { data } = await getDetailsById(
id, // I want to run queries for `id` in _parallel_
pageParam
);
return data;
},
{
getNextPageParam: (lastPage: GetDetailsByIdResponse) =>
lastPage.nextPageToken,
retry: false,
}
);
};
No, I'm afraid there is currently no such thing as useInfiniteQueries.

How to insert a draft-js custom component/block

I'm trying to insert my custom block to the editorState of draft-js's editor. I can't seem to find any detailed information on how to accomplish this.
Block Renderer:
const blockRendererFn = (contentBlock) => {
const type = contentBlock.getType();
if (type === 'CustomTestChipBlock') {
return {
component: CustomTestChipBlock,
editable: false,
props: {
foo: 'bar',
},
};
}
}
Block Render Map:
import { DefaultDraftBlockRenderMap } from "draft-js";
import { Map } from 'immutable';
const blockRenderMap = Map({
CustomTestChipBlock: {
element: 'div',
}
}).merge(DefaultDraftBlockRenderMap);
My custom block (material ui chip):
import { Chip } from "#mui/material";
const CustomTestChipBlock = (props) => {
const { block, contentState } = props;
const { foo } = props.blockProps;
const data = contentState.getEntity(block.getEntityAt(0)).getData();
console.log("foo: "+foo)
console.log("data: "+data)
return (
<Chip label="test" size="small"/>
)
}
Now my problem is when I try to insert my custom block. I assume my method of insertion must be wrong. I tried multiple insertion methods but due to lack of any detailed information on the subject, all of them ended up not even running the console.log inside my custom component.
Insertion:
const addChip = () => {
setEditorState(insertBlock("CustomTestChipBlock"));
}
const insertBlock = (type) => {
// This is where I can't find any detailed info at all
const newBlock = new ContentBlock({
key: genKey(),
type: type,
text: "",
characterList: List(),
});
const contentState = editorState.getCurrentContent();
const newBlockMap = contentState.getBlockMap().set(newBlock.key, newBlock);
const newEditorState = ContentState.createFromBlockArray(
newBlockMap.toArray()
)
.set("selectionBefore", contentState.getSelectionBefore())
.set("selectionAfter", contentState.getSelectionAfter());
return EditorState.push(editorState, newEditorState, "add-chip");
};

Failed to add new elements when set initialState as an empty object

I try to use redux toolkit and I have this as menu-slice.js
I try to use property accessors to add a new property to fileItems, its initial value is an empty object.
import { createSlice } from "#reduxjs/toolkit";
const menuSlice = createSlice({
name: "ui",
initialState: {
fileItems: {},
},
reducers: {
setFileDate: (state, action) => {
state.FileDate = action.payload;
},
replaceFileItems: (state, action) => {
const filesList = action.payload.map((fileName) =>
fileName.slice(fileName.indexOf("/") + 1)
);
state.fileItems[state.FileDate] = filesList;
console.log(`filesList: ${filesList}`);
console.log(`state.fileItems: ${JSON.stringify(state.fileItems)}`);
console.log(`state.FileDate: ${state.FileDate}`);
state.fileContents = null;
},
I call dispatch with the api return value ( dispatch(menuActions.replaceFileItems(fileResponse.data));)
in menu-action.js:
the return value is an array of strings.
export const fetchFiles = (fileDate) => {
return async (dispatch) => {
const fetchFilesList = async () => {
const response = await fetch(
"some url" +
new URLSearchParams({
env: "https://env.com",
date: fileDate,
})
);
if (!response.ok) {
throw new Error("Fail to fetch files list!");
}
const data = await response.json();
return data;
};
try {
const fileResponse = await fetchFilesList();
dispatch(menuActions.setFileDate(FileDate));
dispatch(menuActions.replaceFileItems(fileResponse.data));
} catch (error) {
dispatch(
menuActions.showNotification({....
})
);
}
};
};
But it never prints console logs and didn't display where went wrong in the console or in the chrome redux extension.
I want to add data into state.fileItems on each click that triggers fetchFiles() when it returns a new array:
from state.fileItems = {}
check if state.fileItems already has the date as key,
if not already has the date as key,
change to ex: state.fileItems = {"2022-01-01": Array(2)}
and so on..
ex: state.fileItems = { "2022-01-01": Array(2), "2022-01-02": Array(2) }
I also tried to set state.fileItems as an empty array, and use push, but it didn't work either, nothing printed out, state.fileItems value was always undefined.
Can anyone please tell me why this didn't work?
Thanks for your time to read my question.

How to properly use jasmine-marbles to test multiple actions in ofType

I have an Effect that is called each time it recives an action of more than one "kind"
myEffect.effect.ts
someEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(fromActions.actionOne, fromActions.actionTwo),
exhaustMap(() => {
return this.myService.getSomeDataViaHTTP().pipe(
map((data) =>
fromActions.successAction({ payload: data})
),
catchError((err) =>
ObservableOf(fromActions.failAction({ payload: err }))
)
);
})
)
);
in my test I tried to "simulate the two different actions but I always end up with an error, while if I try with one single action it works perfectly
The Before Each part
describe('MyEffect', () => {
let actions$: Observable<Action>;
let effects: MyEffect;
let userServiceSpy: jasmine.SpyObj<MyService>;
const data = {
// Some data structure
};
beforeEach(() => {
const spy = jasmine.createSpyObj('MyService', [
'getSomeDataViaHTTP',
]);
TestBed.configureTestingModule({
providers: [
MyEffect,
provideMockActions(() => actions$),
{
provide: MyService,
useValue: spy,
},
],
});
effects = TestBed.get(MyEffect);
userServiceSpy = TestBed.get(MyService);
});
This works perfectly
it('should return successActionsuccessAction', () => {
const action = actionOne();
const outcome = successAction({ payload: data });
actions$ = hot('-a', { a: action });
const response = cold('-a|', { a: data });
userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
const expected = cold('--b', { b: outcome });
expect(effects.someEffect$).toBeObservable(expected);
});
This doesn't work
it('should return successAction', () => {
const actions = [actionOne(), actionTwo()];
const outcome = successAction({ payload: data });
actions$ = hot('-a-b', { a: actions[0], b: actions[1] });
const response = cold('-a-a', { a: data });
userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
const expected = cold('--b--b', { b: outcome });
expect(effects.someEffect$).toBeObservable(expected);
});
There are two problems in this code.
It suggests that getSomeDataViaHTTP returns two values. This is wrong, the response is no different from your first example: '-a|'
It expects the second successAction to appear after 40 ms (--b--b, count the number of dashes). This is not correct, because actionTwo happens after 20 ms (-a-a) and response takes another 10 ms (-a). So the first successAction is after 20ms (10+10), the second is after 30ms (20+10). The marble is: '--b-b'.
Input actions : -a -a
1st http response : -a
2nd http response : -a
Output actions : --b -b
The working code:
it('should return successAction', () => {
const actions = [actionOne(), actionTwo()];
actions$ = hot('-a-b', { a: actions[0], b: actions[1] });
const response = cold('-a|', { a: data });
userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
const outcome = successAction({ payload: data });
const expected = cold('--b-b', { b: outcome });
expect(effects.someEffect$).toBeObservable(expected);
});
Marble testing is cool but it involves some black magic you should prepare for. I'd very much recommend you to carefully read this excellent article to have a deeper understanding of the subject.

postgres/bookshelf fetchAll() withRelated (getting multiple relations)

I'm trying to do a fairly complicated (at least for me)API request that involves getting an array of reports. The report object itself has nested objects and so I am trying to pull the related data at the same time. The report object looks like this:
reports: { loading: false, error: null, data: [{id:'', date:'',
student_id:'',
feeding:[],diapering:[], nap:[], meds:[], playTime:[], comments:[],
supplies:[]}] }
I've figured out how to get one report with multiple related tables, but I haven't been able to figure out the syntax for getting an array of reports each with their own related data.
this is what I have at the moment:
exports.getReport = (date) => {
console.log(id)
return Reports.where(date)
.fetchAll({
withRelated: ['feeding', 'comment', 'diapering', 'nap', 'meds', 'playTime', 'supplies']
})
.then(report => {
const meds = report.related('meds')
const nap = report.related('nap')
const feeding = report.related('feeding')
const diapering = report.related('diapering')
const supplies = report.related('supplies')
const playTime = report.related('playTime')
const comm = report.related('comment')
const medsList = meds.map(med => {
return med.attributes
})
const napList = nap.map(n => {
return n.attributes
})
const feedingList = feeding.map(feed => {
return feed.attributes
})
console.log(feedingList)
const diaperList = diapering.map(diaper => {
return diaper.attributes
})
const suppliesList = supplies.map(supply => {
return supply.attributes
})
const playTimeList = playTime.map(play => {
return play.attributes
})
const commentList = comm.map(com => {
return com.attributes
})
return reports
const reports = report.models.map(rep => {
return rep.attributes
})
return [feedingList, commentList, napList, playTimeList,
suppliesList, diaperList, medsList, reports]
})
.catch(err => {
console.log(err)
})
}
this syntax works with a
.fetch()
, but not with the
fetchAll()
I'm getting a
report.related is not a function error