src/components/presenters/addbike-presenter.js
import BasePresenter from './presenter';
import { BikeM } from '../models/export-models'; // Using the BikeModel class because an AddBikeModel class would have the same purpose
import ImageUtil from '../../util/imageutility';
const NO_DATA = 'NO-DATA';
const BIKE_TYPE = ImageUtil.getTypes().BIKE;
/**
* Class for the AddBike presenter and view
* @extends BasePresenter
*/
class AddBikePresenter extends BasePresenter {
/**
* Creates an instance of AddBikePresenter
*
* @constructor
* @param {Object} view - An instance of a view class
*/
constructor(view) {
super();
this.view = view;
this.currentPhotos = Object.assign(ImageUtil.getPhotoEntries(BIKE_TYPE));
// this.currentPhotos = Object.assign(ImageUtil.getPhotoEntries());
BikeM.subscribe(this);
}
/**
* Updates the bike model with new data.
*
* @param {Object} newData - New data to update the model's data with.
* @param {Function} callback - A function that will execute a callback when accessing is complete
*/
update = (newData, callback) => {
const builtData = this._buildDataFromView(newData);
// TODO : Proper checking to see if it was uploaded. Consider adding callback to onUpdated
BikeM.setCallback(callback);
BikeM.update(builtData);
}
/**
* Delete a bike from the database
*
* @param {string} id - A bike id to delete
* @param {Function} callback - A function to call when remove succeeds or fails
*/
deleteBike = (id, callback) => {
BikeM.deleteBikeByID(id, (dataRemoved) => {
// Need to do any processing here?
callback(dataRemoved);
});
}
/**
* Build the data obtained from the view and insert it into a new data object.
* Current attributes of newData object:
* {Object} inputTextData: [{name, multiline, disabled, text}]
* {List} selectedColours
* {Object} picture: uri
*
*
* @param {Object} newData - The new data from the view.
* @return {Object} The built data of the object. Attributes: data
*/
_buildDataFromView = (newData) => {
const inputTextData = newData.inputTextData;
const selectedColours = newData.selectedColours;
const pictureSource = newData.picture;
const currentID = newData.currentID;
// Just add the data into an object. More concise way to do this?
let builtData = {
data: {
id: currentID,
name: inputTextData[inputDataList.index.name].text,
model: inputTextData[inputDataList.index.model].text,
brand: inputTextData[inputDataList.index.brand].text,
colour: selectedColours,
serial_number: inputTextData[inputDataList.index.serial_number].text,
wheel_size: inputTextData[inputDataList.index.wheel_size].text,
frame_size: inputTextData[inputDataList.index.frame_size].text,
notable_features: inputTextData[inputDataList.index.notable_features].text,
thumbnail: pictureSource != null ? pictureSource : [{illustration: ImageUtil.getDefaultImage(BIKE_TYPE)}],
}
}
return builtData;
}
/**
* Called when the model is updated with new data. Refreshes the state of the view.
* Method is supplied with the data to add.
* Better way to refresh the state?
*
* @param {Object} newData - New data to add.
*/
onUpdated = (newData) => {
// Do something with the new data or let the view auto update?
// console.log(newData)
this.view.refreshState();
};
/**
* Gets the data from the model and returns it to the caller.
*
* @return {Object} Data from the model.
*/
getData = () => {
return JSON.parse(JSON.stringify(BikeM.get().data));
};
/**
* If the view or presenter is destroyed, unsubscribe the presenter from the model.
*/
onDestroy = () => {
BikeM.unsubscribe(this);
};
/**
* Checks the editing state of the view and calls one of its passed in functions.
*
* @param {Boolean} editingState - The editing state of the view
* @param {Function} success - A function to call on a true value of the editing state
* @param {Function} failure - A function to call on a false value of the editing state
*/
checkEditingState = (editingState, success, failure) => {
// Do any checks on the editing state
if (editingState) {
success();
} else {
failure();
}
}
/**
* Open the image picker. Set the editing option to true.
*
* @param {Object} imagePicker - The ImagePicker class from react-native-image-picker
* @param {Function} setEditing - A function so the presenter can set the editing value
* @param {Number} id - The index of the photo to change
* @param {List} photos - A list of photos as strings
*/
selectPhotoTapped(imagePicker, setEditing, id, photos) {
const options = {
quality: 1.0,
maxWidth: 500,
maxHeight: 500,
storageOptions: {
skipBackup: true,
},
};
// Set the editing state to true by calling a passed in function where the view can do what it needs to
setEditing(true);
imagePicker.showImagePicker(options, (response) => {
console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled photo picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
} else {
let source = { uri: response.uri };
// You can also display the image using data:
// let source = { uri: 'data:image/jpeg;base64,' + response.data };
photos[id].illustration = source;
this.currentPhotos = photos;
console.log(id, photos);
this.view.setState({
photoEntries: JSON.parse(JSON.stringify(this.currentPhotos)),
});
this.view.refreshState();
}
});
}
/**
* Converts a list of colour objects to a list of objects with a component using the renderer function.
*
* @param {List} colours - A list of colour objects (name, colour)
* @param {Function} renderer - A function that will produce the component to render for this colour
*/
changeText = (colours, renderer) => {
let new_colours = []
let new_item = {}
let count = 0
for (const item of colours) {
const colour = item.colour
new_item.text_component = renderer(colour, item.name); // Render the component using a callback
// <Text style={[{color: colour}, styles.colourText]}>{item.name}</Text>
new_item.name = item.name;
new_colours.push(new_item);
new_item = {} // Need to reset the item because sometimes it doesn't clear
}
// Yeah don't do this, but calling a function in the view doesn't seem to work well either
this.view.setState({
colours: new_colours
});
}
/**
* @private
* Makes sure the object with the key exists.
*/
getProp = (object, key) => object && this.check(object[key]);
/**
* @private
* Simple regex check
*
* return {Boolean}
*/
check = (s) => {
return s.replace(/[\W\[\] ]/g, function (a) {
return a;
})
};
/**
* @private
* This function is an adaptation of the filter function used in SectionedMultiSelect.
* This one filters on uniqueKey instead of displayKey and ignores accents since it is
* a predefined list of colours.
*
* Link: https://github.com/renrizzolo/react-native-sectioned-multi-select/blob/master/exampleapp/App.js#L337
*/
filterItems = (searchTerm, items, { subKey, displayKey, uniqueKey }) => {
let filteredItems = [];
let newFilteredItems = [];
items.forEach((item) => {
const parts = searchTerm.trim().split(/[[ \][)(\\/?\-:]+/);
const regex = new RegExp(`(${parts.join('|')})`, 'i');
if (regex.test(this.getProp(item, uniqueKey))) {
filteredItems.push(item);
}
if (item[subKey]) {
const newItem = Object.assign({}, item);
newItem[subKey] = [];
item[subKey].forEach((sub) => {
if (regex.test(this.getProp(sub, uniqueKey))) {
newItem[subKey] = [...newItem[subKey], sub];
newFilteredItems = this.rejectProp(filteredItems, singleItem =>
item[uniqueKey] !== singleItem[uniqueKey]);
newFilteredItems.push(newItem);
filteredItems = newFilteredItems;
}
})
}
})
return filteredItems
}
/**
* Checks the input data for required inputs and calls an alert function if inputs are missing.
*
* @param {List} inputData - A list of input data (see inputDataList for structure)
* @param {Function} inputRequirementFailure - A function that will define the alert to be displayed.
* @return {Boolean} true: some required inputs are blank; false: required inputs are not blank
*/
checkInputs = (inputData, inputRequirementFailure) => {
const all_defaults = ImageUtil.checkPhotosForDefaults(BIKE_TYPE, this.currentPhotos);
let required = this._getRequiredInputs(inputData);
let names = [];
for (let i=0; i < required.length; i++) {
if (required[i].text === "") { // If it's empty then push
names.push(required[i].name);
}
}
if (names.length !== 0 || all_defaults) { // If inputs or images were empty, call the callback
all_defaults ? names.push('Images') : '';
inputRequirementFailure(names);
return false;
} else {
return !!(true & !all_defaults); // !! converts to boolean because '&' converts to number
}
}
/**
* Returns the required inputs based on the required property
*
* @param {List} inputs - A list of input data
* @return {List} A list of required text inputs
*/
_getRequiredInputs = (inputs) => {
return inputs.filter(obj => {return obj.required});
}
/**
* Return the data for the text inputs.
* Object:
* name: the name/label of the text input
* disabled: true: if the field is disabled; false: otherwise
* multiline: true: if the input is allowed to span multiple lines; false: otherwise
* text: initial text of the input
*
* @param {Object} data - type 'Object' if there is data, type 'string' if no data
* @param {Boolean} isEditPage - true: If the current page is 'Edit Bike' page; false: If 'Add Bike' page
* @return {List} A list of data objects (name, multiline, text)
*/
getTextInputData = (data, isEditPage) => {
// Do something with isEditPage to make sure that data isn't cleared that can't be edited.
// For example, if we don't want user to edit the Serial Number, it would be disabled and not cleared when the 'Clear' button is clicked
return data === NO_DATA ? this._deepCopy(inputDataList.data) : this._translateDataToInput(data);
}
/**
* Translates data input (Bike data) to the text inputs. Could be refactored to be made easier for adaptations.
*
* @param {Object} data - The data from the view (=== 'NO-DATA' if not set)
* @return {List} A copy of the data that is now in the form of the text input
*/
_translateDataToInput = (data) => {
let dataCopy = this._deepCopy(inputDataList.data);
// To be safe, convert data to string
dataCopy[inputDataList.index.name].text = this._getString(data.name);
dataCopy[inputDataList.index.serial_number].text = this._getString(data.serial_number);
dataCopy[inputDataList.index.brand].text = this._getString(data.brand);
dataCopy[inputDataList.index.model].text = this._getString(data.model);
dataCopy[inputDataList.index.notable_features].text = this._getString(data.notable_features);
dataCopy[inputDataList.index.wheel_size].text = this._getString(data.wheel_size);
dataCopy[inputDataList.index.frame_size].text = this._getString(data.frame_size);
const thumbnail = ImageUtil.formThumbnail(data.thumbnail);
this.currentPhotos = ImageUtil.addRemainingDefaults(BIKE_TYPE, thumbnail);
this.view.setState({ currentID: data.id });
return this._deepCopy(dataCopy);
}
/**
* Checks if the value is valid and if so, convert it to a string.
*
* @param {Number/string} val - A number or string to check
* @return {string} Value converted to a string
*/
_getString = (val) => {
return val == undefined || val == null ? '' : val.toString();
}
/**
* Resets the current photos to the default photos.
*/
clearPhotos = () => {
this.currentPhotos = ImageUtil.getDefaultPhotos(BIKE_TYPE);
}
/**
* Returns a deep copy of the current photos.
*
* @return {List} A list of the current photos
*/
getCurrentPhotos = () => {
return JSON.parse(JSON.stringify(this.currentPhotos));
}
/**
* Returns a deep copy of the array by reassigning the values. This is to make sure we can clear the data.
*
* @return {List} A list to copy
*/
_deepCopy = (array) => {
return array.map(a => Object.assign({}, a));
}
/**
* Toggles the colours from the data if the data is present.
*
* @param {Object} sectionedMultiSelect - The multi select component from the view
* @param {Object} data - The data from the view (=== 'NO-DATA' if not set)
* @param {Function} onColoursFound - A function that submits the selected items back to the view
* @param {string} UNIQUE_KEY - A unique key that is used to get the data from the item (same one that is used when defining the sectioned select)
*/
toggleColours = (sectionedMultiSelect, data, onColoursFound, UNIQUE_KEY) => {
let selectedItems = [];
if (data !== NO_DATA) {
for (const colour of data.colour) {
item = sectionedMultiSelect._findItem(colour);
sectionedMultiSelect._itemSelected(item);
sectionedMultiSelect._toggleItem(item, false);
selectedItems.push(item[UNIQUE_KEY]); // Unique key corresponding to sectioned list
}
onColoursFound(selectedItems);
}
}
}
export default AddBikePresenter;
// List of text inputs for adding bike. Items in list appear in this order
/*
* data:
* name: The name/label of the text input
* disabled: true: if the user can edit the field; false: otherwise
* multiline: true: if the user's text can span multiple lines; false: otherwise
* text: The initial text of the name/label
*/
const inputDataList = {
index: {
brand: 0,
model: 1,
serial_number: 2,
notable_features: 3,
wheel_size: 4,
frame_size: 5,
name: 6,
},
data: [
{
name: 'Brand',
multiline: false,
disabled: false,
required: true,
text: ''
},
{
name: 'Model',
multiline: false,
disabled: false,
required: true,
text: ''
},
{
name: 'Serial Number',
multiline: false,
disabled: false,
required: true,
text: ''
},
{
name: 'Notable Features',
multiline: true,
disabled: false,
required: false,
text: ''
},
{
name: 'Wheel Size',
multiline: false,
disabled: false,
required: false,
text: ''
},
{
name: 'Frame Size',
multiline: false,
disabled: false,
required: false,
text: ''
},
{
name: 'Nickname',
multiline: false,
disabled: false,
required: false,
text: '',
}
]
}