Files
P42_UI/app/libs/Windoz/WindozPluralModel.js
2025-10-14 16:31:07 +00:00

246 lines
8.9 KiB
JavaScript

/**
* @author Nicolas Stein
* @category MyEic
* @subcategory Libraries
* @extends WindozModel
*/
class WindozPluralModel extends WindozModel {
constructor(businessObject, privileges, singletonClass) {
super(businessObject, privileges);
this.collection = [];
//this.api = app.config.api.filter(item => item.businessObject == businessObject);
this.singletonClass = singletonClass;
}
/**
* Get all ids in the current collection
* @return {Array} Ids (keys) of the current collection.
*/
get ids() { return this.collection.map(item => item.itemData.id); }
/**
* Creates empty singletons at the end of the collection.
* @param {Array} ids - Ids of the singletons to create
*/
create(ids){
for(let id of ids){
this.collection[id] =new this.singletonClass;
}
}
/**
* Converts the data in a singleton & adds it to the collection.
* If the id exists, will just override keys from the server (preserving eventual overrides)
* @param {Object} data - key-value data used to create the singleton.
* @return {Object} The collection length after insertion.
*/
add(data, id=null){
id = data.id || null;
let item = this.collection.find(item => item.id == id);
if(item){
Object.assign(item.itemData, data);
} else {
item = new this.singletonClass();
item.itemData = data;
this.collection.push(item);
}
return(item);
}
/**
* Removes and returns a singleton out of the collection.
* @param {string} id - id of the singleton to pop.
* @return {Object} The popped singleton.
*/
pop(id) {
if(id in this.collection) {
let singleton = this.collection[id];
this.delete(this.collection[id]);
return(singleton);
} else return(null);
}
/**
* Returns the number of singletons in the collection.
* @return {Int} Number of singletons.
*/
get length() { return this.collection.length; }
/**
* Empties the collection
*/
empty() { this.collection = []; }
/**
* Search the collection for matching singletons
* @param {Object} matchValues - { {string} key: {string} valueToMatch }
* @param {Object} matchExact - { strin{key}: {Boolean} } Defaults to exact match if parameter absent or if key absent
* @return {Array} Array of matching singletons.
*/
localFind(matchValues, matchExact){
return(
this.collection.filter((singleton) => singleton.match(matchValues, matchExact))
);
}
/**
* Loads singletons from their IDs at the end of the current collection.
* if no ids given, loads all
* Expected server-contract:
* [ {"id":"xxx", ...other singleton data...}, {"id":"yyy", ...other singleton data...}, ...]
* @param {array} ids - singleton ids to be loaded
* @return {promise} Promise returning the model
*/
load(endpoint, payload){
return(
this.request(endpoint.uri, endpoint.method, payload)
.then(async serverData => {
this.fill(serverData);
return(this);
})
)
}
fill(serverData) {
if(serverData && serverData.length){
for(let singletonData of serverData) this.add(this.sanitize(singletonData));
}
}
sanitize(item) { return item; }
/**
* Loads singletons from search criteria at the end of the current collection.
* @param {Object} matchValues - { {string} key: {string} valueToMatch }
* @param {Object} matchExact - { strin{key}: {Boolean} } Defaults to exact match if parameter absent or if key absent
* @return {Array} Array of matching singletons.
*/
remotefind(matchValues, matchExact){
return(
this.post(this.baseUrl+'search', { 'matchValues': matchValues, 'matchExact': matchExact })
.then(async serverData => {
if(serverData){
for(let singletonData of serverData) this.add(singletonData);
}
return(this);
})
)
}
save(){
return(
this.put(this.baseUrl, this.collection)
.then(async serverData => {
if(serverData){
for(let singletonData of data) this.add(singletonData);
}
return(this);
})
)
}
/**
* Fills the collection from a raw CSV string.
* (adapted from https://stackoverflow.com/users/10549827/matthew-e-brown)
* @param {string} csv The raw CSV string.
* @param {string[]} headers An optional array of headers to use. If none are
* given, they are pulled from the first line of `text`.
* @param {string} idName The name of the column to be used as id.
* @param {string} quoteChar A character to use as the encapsulating character.
* @param {string} delimiter A character to use between columns.
*/
//TODO NIKE: test ! & understand why in accumulator, merge uses [key] instead of key
fromCsv(csv, headers = null, idName='id', quoteChar = '"', delimiter = ',') {
const regex = new RegExp(`\\s*(${quoteChar})?(.*?)\\1\\s*(?:${delimiter}|$)`, 'gs');
const match = line => [...line.matchAll(regex)]
.map(m => m[2]) // we only want the second capture group
.slice(0, -1); // cut off blank match at the end
const lines = csv.split('\n');
const firstLine = lines.shift();
const heads = headers || match(firstLine);
if(heads.indexOf(idName)<0) {
console.warn(`id column "${idName}" not found in CSV headers !`);
return;
}
heads[heads.indexOf(idName)]='id'; let cols;
for(let line of lines){
cols = match(line);
if(cols.length>=heads.length){
let singletonData = cols.reduce((acc, cur, i) => {
// Attempt to parse as a number; replace blank matches with `null`
const val = cur.length <= 0 ? null : Number(cur) || cur;
const key = heads[i] [[]] `extra_${i}`;
return { ...acc, [key]: val };
}, {});
this.add(singletonData);
}
};
}
/**
* Builds a CSV in a string from the collection.
* @param {Array} keys model properties you want to include in the csv. It defaults to the least common denominator of properties across the collection.
* @param {boolean} withHeaders If true, the first line has the properties names.
* @param {string} quoteChar Will surround all values (and properties if withHeader).
* @param {string} delimiter A character to use between columns.
*/
//TODO NIKE: test !
toCsv(keys=null, withHeader=true, quoteChar = '"', delimiter = ';', downloadName=null){
const _dwnldCsv = function(data, fname) {
const blob = new Blob([data], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.setAttribute('href', url);
a.setAttribute('download', fname+'.csv');
a.click();
}
if(!keys) { // take min. common set of keys in collection (take 1st then successively intersect to itself)
let ids = Object.keys(this.collection);
keys = Object.keys(this.collection[ids[0]].itemData);
for(let id of ids) {
keys = keys.filter(x => (this.collection[id].itemData.hasOwnProperty(x)))
}
}
let csv=''; let row=[];
if(withHeader===true){
for(let key of keys){
row.push(quoteChar+key+quoteChar);
}
csv += row.join(delimiter)+'\n';
} else if(Array.isArray(withHeader)) {
for(let title of withHeader){
row.push(quoteChar+title+quoteChar);
}
csv += row.join(delimiter)+'\n';
}
for(let id in this.collection){
row = [];
for(let key of keys){
let item = ''
if(typeof(key)=='string'){ // normal column
item = this.collection[id].itemData
for(let k of key.split('.')) { // Allow for sub-objects with dotted keys notation
if(!item) break
item = item[k]
}
} else if(typeof(key)=='function') { // computed column
item = key(this.collection[id].itemData)
} else console.warn('CSV: Bad column: ',key)
row.push(quoteChar+item+quoteChar);
}
csv += row.join(delimiter)+'\n';
}
if(downloadName) _dwnldCsv(csv, downloadName)
return(csv);
}
}
app.registerClass('WindozPluralModel',WindozPluralModel);