src/app/shared/forms/tags.service.ts
import { Injectable } from '@angular/core';
import { CouchService } from '../couchdb.service';
import { of, forkJoin } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../state.service';
import { findDocuments } from '../mangoQueries';
import { createDeleteArray } from '../table-helpers';
@Injectable({
providedIn: 'root'
})
export class TagsService {
constructor(
private couchService: CouchService,
private stateService: StateService,
) {}
getTags(db: string, parent: boolean) {
return forkJoin([
this.stateService.getCouchState('tags', parent ? 'parent' : 'local', { 'name': 'asc' }),
this.stateService.getCouchState(db, parent ? 'parent' : 'local')
]).pipe(
map(([ tags, linkDocs ]: [ any[], any[] ]) => {
const tagCounts = tags.filter(tag => tag.linkId === undefined || linkDocs.some(linkDoc => linkDoc._id === tag.linkId))
.reduce(
(counts: any, tag: any) => tag.linkId === undefined ? counts : { ...counts, [tag.tagId]: (counts[tag.tagId] || 0) + 1 },
{}
);
return tags
.map((tag: any) => ({ ...tag, count: tagCounts[tag._id] || 0 }))
.filter((tag: any) => tag.db === db && tag.docType === 'definition')
.map(this.fillSubTags);
})
);
}
filterTags(tags: any[], filterString: string): string[] {
// Includes any tag with a sub tag that matches in addition to tags that match
const tagTest = (tag) => tag.name.toLowerCase().indexOf(filterString.toLowerCase()) > -1;
return tags.reduce((newTags, tag) => {
const newTag = { ...tag, subTags: (tag.subTags || []).filter(tagTest) };
if (tagTest(tag)) {
newTags.push(tag);
} else if (newTag.subTags.length > 0) {
newTags.push(newTag);
}
return newTags;
}, []);
}
updateTag(tag) {
const { count, subTags, ...tagData } = tag;
tagData.attachedTo = tagData.attachedTo || [];
const newId = `${tagData.attachedTo.length === 0 ? tagData.db : tagData.attachedTo}_${tagData.name.toLowerCase()}`;
if (newId === tag._id) {
return this.couchService.updateDocument('tags', tagData).pipe(
switchMap(res => of([ res ]))
);
}
return (tag._id ?
this.couchService.findAll('tags', findDocuments({ '$or': [ { 'tagId': tag._id }, { 'attachedTo': tag._id } ] })) :
of([])
).pipe(
switchMap((oldLinks: any[]) => {
const replaceField = (oldValue, newValue) => oldValue === undefined ? undefined : newValue;
const newLinks = oldLinks.map(t => ({ ...t, tagId: replaceField(t.tagId, newId), attachedTo: replaceField(t.attachedTo, newId) }));
return this.couchService.bulkDocs('tags', [
{ ...tagData, _rev: undefined, _id: newId },
{ ...tagData, _deleted: true },
...newLinks
]);
})
);
}
deleteTag(tag) {
return this.couchService.findAll('tags', findDocuments({ 'tagId': tag._id })).pipe(
switchMap((tags: any[]) => {
const deleteTagsArray = createDeleteArray(tags);
return this.couchService.bulkDocs('tags', [ ...deleteTagsArray, { ...tag, _deleted: true } ]);
})
);
}
findTag(tagKey: any, fullTags: any[]) {
const fullTag = fullTags.find((dbTag: any) => dbTag._id === tagKey);
return { ...(fullTag ? fullTag : { _id: tagKey, name: tagKey, attachedTo: [] }) };
}
fillSubTags(tag: any, index: number, tags: any[]) {
return { ...tag, subTags: tags.filter(({ attachedTo }) => (attachedTo || []).indexOf(tag._id) > -1) };
}
attachTagsToDocs(db: string, docs: any[], tags: any[]) {
const tagsObj = tags.reduce((obj, tagLink: any) => {
if (tagLink.docType !== 'link' || tagLink.db !== db) {
return obj;
}
const tag = { ...this.findTag(tagLink.tagId, tags), tagLink };
return ({ ...obj, [tagLink.linkId]: obj[tagLink.linkId] ? [ ...obj[tagLink.linkId], tag ] : [ tag ] });
}, {});
return docs.map((doc: any) => {
const docTags = tagsObj[doc._id] || [];
return {
...doc,
tags: docTags.map(tag => ({
...tag,
subTags: tag.subTags ? tag.subTags.filter(subTag => docTags.some(docTag => docTag._id === subTag._id)) : [],
isMainTag: this.filterOutSubTags(tag)
}))
};
});
}
tagBulkDocs(linkId: string, db: string, newTagIds: string[], currentTags: any[] = []) {
// name property is needed for tags database queries
const tagLinkDoc = (tagId) => ({ linkId, tagId, name: '', docType: 'link', db });
return [
...newTagIds.filter(tagId => currentTags.findIndex((tag: any) => tag.tagId === tagId) === -1)
.map(tagId => tagLinkDoc(tagId)),
...currentTags.filter((tag: any) => newTagIds.indexOf(tag.tagId) === -1)
.map((tag: any) => ({ ...tag.tagLink, '_deleted': true }))
];
}
updateManyTags(data, dbName, { selectedIds, tagIds, indeterminateIds }) {
const fullSelectedTags = tagIds.filter(tagId => indeterminateIds.indexOf(tagId) === -1);
const items = selectedIds.map(id => data.find((item: any) => item._id === id));
const newTags = items.map((item: any) =>
this.tagBulkDocs(
item._id, dbName, fullSelectedTags, item.tags.filter((tag: any) => indeterminateIds.indexOf(tag._id) === -1)
)
).flat();
return this.couchService.bulkDocs('tags', newTags);
}
filterOutSubTags(tag: any) {
return tag.attachedTo === undefined || tag.attachedTo.length === 0;
}
}