Home Reference Source

cables_dev/cables_ui/src/ui/components/opsearch.js

  1. import { Events } from "cables-shared-client";
  2. import { CgContext } from "cables-corelibs";
  3. import { Link, Patch, Port, utils } from "cables";
  4. import defaultOps from "../defaultops.js";
  5. import namespace from "../namespaceutils.js";
  6. import opNames from "../opnameutils.js";
  7. import { gui } from "../gui.js";
  8. import { platform } from "../platform.js";
  9. /**
  10. * @typedef OpSearchListItem
  11. * @property {string|number} opId simpleId if op has no doc
  12. * @property {string} name full op name, e.g. Ops.Math.Sum
  13. * @property {string} summary
  14. * @property {string} [description] collections only
  15. * @property {string} [collectionOpNames] lowercase, space separated op names of a collection
  16. * @property {string} nscolor namespace css class name
  17. * @property {boolean} isOp false for collections
  18. * @property {boolean} userOp
  19. * @property {boolean} devOp
  20. * @property {boolean} extensionOp
  21. * @property {boolean} teamOp
  22. * @property {boolean} patchOp
  23. * @property {boolean} isExtension
  24. * @property {boolean} isTeamNamespace
  25. * @property {boolean} isCollection
  26. * @property {string} shortName
  27. * @property {string} nameSpace
  28. * @property {string[]} namespaces all parent namespaces, e.g. ["Ops.Math"]
  29. * @property {string} oldState "", "OLD" or "DEPREC"
  30. * @property {string} lowercasename
  31. * @property {string} buttonText
  32. * @property {string} type "op", "team", "extension" or "patchop"
  33. * @property {number} pop popularity
  34. * @property {string} [teamName] collections only
  35. * @property {string} [teamLink] collections only
  36. * @property {number} [numOps] collections only
  37. * @property {string[]} [ops] collections only
  38. * @property {number} [id] index in list, set by buildList
  39. * @property {boolean} [old] deprecated or outdated version
  40. * @property {string} [abbrev] abbreviation of shortName
  41. * @property {string} [_summary] lowercase search cache
  42. * @property {string} [_shortName] lowercase search cache
  43. * @property {string} [_lowerCaseName] lowercase search cache
  44. * @property {string} [_nameSpace] lowercase search cache
  45. * @property {string} [_nameSpaceFull] lowercase search cache
  46. * @property {number} [score] search score
  47. * @property {string} [scoreDebug] html explaining the score
  48. * @property {HTMLElement} [element] result element in opselect
  49. * @property {boolean} [elementHidden]
  50. */
  51. /** @typedef NewOpOptions
  52. * @property {Link} [linkNewLink]
  53. * @property {Port} [linkNewOpToPort]
  54. */
  55. /** Searches through the op docs, e.g. for the op select dialog. */
  56. export default class OpSearch extends Events
  57. {
  58. /** @type {NewOpOptions} */
  59. newOpOptions = {};
  60. constructor()
  61. {
  62. super();
  63. /** @type {OpSearchListItem[]} */
  64. this._list = null;
  65. this._wordsDb = null;
  66. this.numPatchops = 0;
  67. this.originalSearch = "";
  68. this.hideUserOps = false;
  69. }
  70. /** @returns {OpSearchListItem[]} */
  71. get list()
  72. {
  73. return this._list;
  74. }
  75. resetList()
  76. {
  77. this._list = null;
  78. }
  79. buildList()
  80. {
  81. const perf = gui.uiProfiler.start("opsearch.getlist");
  82. const codeOpNames = this._getOpsNamesFromCode([], "Ops", Ops, "");
  83. let items = this._createListItemsByNames(codeOpNames);
  84. const docOpName = gui.opDocs.getOpDocs().map((ext) => { return ext.name; });
  85. items = items.concat(this._createListItemsByNames(docOpName, items));
  86. const extensionNames = gui.opDocs.getExtensions().map((ext) => { return ext.name; });
  87. items = items.concat(this._createListItemsByNames(extensionNames, items));
  88. const teamNamespaces = gui.opDocs.getTeamNamespaces().map((ext) => { return ext.name; });
  89. items = items.concat(this._createListItemsByNames(teamNamespaces, items));
  90. const ns = platform.getPatchOpsNamespace();
  91. const patchOpNames = gui.opDocs.getNamespaceDocs(ns).map((ext) => { return ext.name; });
  92. this.numPatchops = utils.uniqueArray(patchOpNames || []).length;
  93. items = items.concat(this._createListItemsByNames(patchOpNames, items));
  94. const newList = {};
  95. items.forEach((item) =>
  96. {
  97. if (!newList.hasOwnProperty(item.opId))
  98. {
  99. newList[item.opId] = item;
  100. }
  101. });
  102. this._list = Object.values(newList);
  103. this._list.sort((a, b) => { return b.pop - a.pop; });
  104. perf.finish();
  105. /// --------------
  106. let maxPop = 0;
  107. for (let i = 0; i < this._list.length; i++)
  108. {
  109. if (!this._list[i].shortName) this._list[i].shortName = this._list[i].name;
  110. maxPop = Math.max(this._list[i].pop || 0, maxPop);
  111. this._list[i].id = i;
  112. this._list[i].summary = this._list[i].summary || "";
  113. this._list[i]._summary = this._list[i].summary.toLowerCase();
  114. this._list[i]._shortName = this._list[i].shortName.toLowerCase();
  115. this._list[i]._lowerCaseName = this._list[i].name.toLowerCase();
  116. this._list[i]._nameSpace = this._list[i].nameSpace.toLowerCase() + ".";
  117. this._list[i]._nameSpaceFull = this._list[i].nameSpace.toLowerCase() + "." + this._list[i].shortName.toLowerCase();
  118. const opdoc = gui.opDocs.getOpDocByName(this._list[i].name);
  119. if (namespace.isDeprecatedOp(this._list[i].name) || (opdoc && opdoc.oldVersion)) this._list[i].old = true;
  120. }
  121. this._rebuildWordList();
  122. CABLES.UI.OPSELECT.maxPop = maxPop;
  123. }
  124. /**
  125. * @param {number} wordIndex
  126. * @param {string} orig
  127. * @param {OpSearchListItem[]} list
  128. * @param {string} query
  129. */
  130. _searchWord(wordIndex, orig, list, query)
  131. {
  132. if (!query || query === " " || query === "") return;
  133. const perf = gui.uiProfiler.start("opsearch._searchWord");
  134. for (let i = 0; i < list.length; i++)
  135. {
  136. if (wordIndex > 0 && list[i].score === 0) continue; // when second word was found, but first was not
  137. let scoreDebug = "<b>Query: " + query + " </b><br/>";
  138. let found = false;
  139. let points = 0;
  140. if (list[i].lowercasename.indexOf(query) > -1)
  141. {
  142. if (list[i].name === defaultOps.glMainloop)
  143. {
  144. found = true;
  145. scoreDebug += "+2 vip op<br/>";
  146. points += 2;
  147. }
  148. }
  149. if (this.preferGApi == CgContext.API_WEBGL)
  150. {
  151. if (list[i].name.startsWith(defaultOps.prefixes.webgpu))
  152. {
  153. found = true;
  154. scoreDebug += "-5 different graphics api<br/>";
  155. points -= 5;
  156. }
  157. }
  158. else if (this.preferGApi == CgContext.API_WEBGPU)
  159. {
  160. if (list[i].name.startsWith(defaultOps.prefixes.webgl))
  161. {
  162. found = true;
  163. scoreDebug += "-5 different graphics api<br/>";
  164. points -= 5;
  165. }
  166. }
  167. if (list[i].abbrev && list[i].abbrev.indexOf(orig) === 0)
  168. {
  169. found = true;
  170. let p = 4;
  171. if (orig.length === 2)p = 12;
  172. if (orig.length === 3)p = 10;
  173. if (orig.length === 4)p = 8;
  174. if (query === query.toUpperCase())
  175. {
  176. const ppo = 5;
  177. p += ppo;
  178. scoreDebug += "+" + ppo + " uppercase abbreviation<br/>";
  179. }
  180. scoreDebug += "+" + p + " abbreviation<br/>";
  181. points += p;
  182. }
  183. if (list[i].userOp && this.hideUserOps) continue;
  184. if (list[i]._summary.indexOf(query) > -1)
  185. {
  186. found = true;
  187. points += 1;
  188. scoreDebug += "+1 found in summary (" + query + ")<br/>";
  189. }
  190. if (list[i]._nameSpace.indexOf(query) > -1)
  191. {
  192. found = true;
  193. points += 1;
  194. scoreDebug += "+1 found in namespace (" + query + ")<br/>";
  195. }
  196. if (list[i]._shortName.indexOf(query) > -1)
  197. {
  198. found = true;
  199. points += 4;
  200. scoreDebug += "+4 found in shortname (" + query + ")<br/>";
  201. }
  202. if (list[i]._shortName.startsWith(query))
  203. {
  204. found = true;
  205. points += 2;
  206. scoreDebug += "+2 query startswith shortname<br/>";
  207. }
  208. if (list[i]._shortName.endsWith(query))
  209. {
  210. found = true;
  211. points += 2;
  212. scoreDebug += "+2 query endsWith shortname<br/>";
  213. }
  214. if (orig.length > 1 && list[i]._lowerCaseName.indexOf(orig) > -1)
  215. {
  216. found = true;
  217. points += 2;
  218. scoreDebug += "+2 found full namespace (" + query + ")<br/>";
  219. }
  220. if (points == 0)
  221. {
  222. if (list[i]._lowerCaseName.indexOf(query) > -1)
  223. {
  224. found = true;
  225. points += 2;
  226. scoreDebug += "+2 found full namespace (" + query + ")<br/>";
  227. }
  228. }
  229. if (list[i].collectionOpNames && list[i].collectionOpNames.indexOf(orig) > -1)
  230. {
  231. found = true;
  232. points += 1;
  233. scoreDebug += "+1 is op in collection (" + query + ")<br/>";
  234. }
  235. if (found)
  236. {
  237. if (this.newOpOptions)
  238. {
  239. const firstportfitspoints = 3;
  240. const firstportfitsText = "+3 First Port fits<br/>";
  241. const docs = gui.opDocs.getOpDocByName(list[i].name);
  242. // if (docs && docs.hasOwnProperty("version"))
  243. // {
  244. // const p = docs.version * 0.01;
  245. // points += p;
  246. // scoreDebug += "+" + p + " version<br/>";
  247. // }
  248. /// ////////////////////
  249. // namespace similarity
  250. let linkPortIn = null;
  251. let linkPortOut = null;
  252. if (this.newOpOptions.linkNewLink)
  253. {
  254. linkPortIn = this.newOpOptions.linkNewLink.portIn;
  255. linkPortOut = this.newOpOptions.linkNewLink.portOut;
  256. }
  257. let nspoints = 0;
  258. if (this.newOpOptions.linkNewOpToPort)
  259. {
  260. if (this.newOpOptions.linkNewOpToPort.op)
  261. for (let nsi = 0; nsi < list[i].namespaces.length; nsi++)
  262. {
  263. if (!this.newOpOptions.linkNewOpToPort.op.objName.startsWith(list[i].namespaces[nsi])) break;
  264. nspoints++;
  265. }
  266. }
  267. if (list[i].nameSpace.startsWith("Ops.Graphics") &&
  268. (this.newOpOptions.linkNewOpToPort && this.newOpOptions.linkNewOpToPort.op.objName.startsWith("Ops.Gl")) ||
  269. (linkPortIn && linkPortIn.op.objName.startsWith("Ops.Gl")) ||
  270. (linkPortIn && linkPortOut.op.objName.startsWith("Ops.Gl")))
  271. {
  272. scoreDebug += "+2 compatible namespace similarity<br/>";
  273. nspoints += 2;
  274. }
  275. if (linkPortIn)
  276. for (let nsi = 0; nsi < list[i].namespaces.length; nsi++)
  277. {
  278. if (!linkPortIn.op.objName.startsWith(list[i].namespaces[nsi])) break;
  279. nspoints++;
  280. }
  281. if (linkPortOut)
  282. for (let nsi = 0; nsi < list[i].namespaces.length; nsi++)
  283. {
  284. if (!linkPortOut.op.objName.startsWith(list[i].namespaces[nsi])) break;
  285. nspoints++;
  286. }
  287. if (nspoints > 0)
  288. {
  289. points += nspoints;
  290. scoreDebug += "+" + nspoints + " namespace similarity<br/>";
  291. }
  292. /// ////////////////////
  293. if (docs && docs.layout && docs.layout.portsIn && docs.layout.portsOut && docs.layout.portsIn.length > 0 && docs.layout.portsOut.length > 0)
  294. {
  295. // when inserting into link - find fitting ports
  296. if (linkPortIn && linkPortOut)
  297. {
  298. let foundPortTypeIn = false;
  299. for (let j = 0; j < docs.layout.portsIn.length; j++)
  300. {
  301. if (docs.layout.portsIn[j] && docs.layout.portsIn[j].type == linkPortIn.type)
  302. {
  303. foundPortTypeIn = true;
  304. break;
  305. }
  306. }
  307. let foundPortTypeOut = false;
  308. for (let j = 0; j < docs.layout.portsOut.length; j++)
  309. {
  310. if (docs.layout.portsOut[j] && docs.layout.portsOut[j].type == linkPortOut.type)
  311. {
  312. foundPortTypeOut = true;
  313. break;
  314. }
  315. }
  316. if (
  317. docs.layout.portsIn[0] && docs.layout.portsOut[0] &&
  318. docs.layout.portsIn[0].type == linkPortOut.type &&
  319. docs.layout.portsOut[0].type == linkPortIn.type
  320. )
  321. {
  322. points += firstportfitspoints;
  323. scoreDebug += firstportfitsText;
  324. }
  325. if (!foundPortTypeOut && !foundPortTypeIn)
  326. {
  327. points -= 5.0; // seems harsh, but is only used when dragging a port, so it should be fine...
  328. scoreDebug += "-5.0 no compatible port found<br/>";
  329. }
  330. }
  331. // when dragging a port - find fitting input/output port
  332. if (this.newOpOptions.linkNewOpToPort)
  333. {
  334. let foundPortType = false;
  335. if (this.newOpOptions.linkNewOpToPort.direction === Port.DIR_OUT)
  336. {
  337. if (docs.layout.portsIn[0] && docs.layout.portsIn[0].type == this.newOpOptions.linkNewOpToPort.type)
  338. {
  339. points += firstportfitspoints;
  340. scoreDebug += firstportfitsText;
  341. }
  342. for (let j = 0; j < docs.layout.portsIn.length; j++)
  343. {
  344. if (docs.layout.portsIn[j] && docs.layout.portsIn[j].type == this.newOpOptions.linkNewOpToPort.type)
  345. {
  346. foundPortType = true;
  347. break;
  348. }
  349. }
  350. }
  351. else
  352. {
  353. if (docs.layout.portsOut[0] && docs.layout.portsOut[0].type == this.newOpOptions.linkNewOpToPort.type)
  354. {
  355. points += firstportfitspoints;
  356. scoreDebug += firstportfitsText;
  357. }
  358. for (let j = 0; j < docs.layout.portsOut.length; j++)
  359. {
  360. if (docs.layout.portsOut[j] && docs.layout.portsOut[j].type == this.newOpOptions.linkNewOpToPort.type)
  361. {
  362. foundPortType = true;
  363. break;
  364. }
  365. }
  366. }
  367. if (!foundPortType)
  368. {
  369. points -= 10.0; // seems harsh, but is only used when dragging a port, so it should be fine...
  370. scoreDebug += "-10.0 no compatible port found<br/>";
  371. }
  372. }
  373. }
  374. }
  375. if (list[i]._shortName.indexOf(orig) === 0)
  376. {
  377. points += 2.5;
  378. scoreDebug += "+2.5 found in shortname at beginning (" + query + ")<br/>";
  379. if (list[i]._shortName == orig)
  380. {
  381. points += 2;
  382. scoreDebug += "+2 exact name (" + query + ")<br/>";
  383. }
  384. }
  385. if (list[i]._nameSpace.indexOf("ops.math") > -1)
  386. {
  387. points += 1;
  388. scoreDebug += "+1 is math op (" + query + ")<br/>";
  389. }
  390. else if (list[i]._nameSpace.indexOf("ops.patch") > -1)
  391. {
  392. points += 3;
  393. scoreDebug += "+1 is patch op (" + query + ")<br/>";
  394. }
  395. else if (list[i]._nameSpace.indexOf("ops.team") > -1)
  396. {
  397. points += 2;
  398. scoreDebug += "+2 is team op (" + query + ")<br/>";
  399. }
  400. const shortnessPoints = 2 * Math.round((1.0 - Math.min(1, (list[i]._nameSpace + list[i]._shortName).length / 100)) * 100) / 100;
  401. points += shortnessPoints;
  402. scoreDebug += "+" + shortnessPoints + " shortness namespace<br/>";
  403. }
  404. if (found && this._list[i].old)
  405. {
  406. points -= 1;
  407. scoreDebug += "-1 outdated<br/>";
  408. }
  409. if (found && list[i].pop > 0)
  410. {
  411. points += (list[i].pop || 2) / CABLES.UI.OPSELECT.maxPop || 1;
  412. }
  413. if (!found) points = 0;
  414. if (points === 0 && list[i].score > 0) list[i].score = 0;
  415. else list[i].score += points;
  416. list[i].scoreDebug = (list[i].scoreDebug || "") + scoreDebug + " (" + Math.round(points * 100) / 100 + " points)<br/><br/>";
  417. }
  418. perf.finish();
  419. }
  420. /**
  421. * @param {string} query
  422. * @param {string} [originalSearch]
  423. */
  424. search(query, originalSearch)
  425. {
  426. this.preferGApi = gui.canvasManager.currentContextCg()?.gApi;
  427. document.getElementById("realsearch").innerHTML = "";
  428. document.getElementById("opOptions").innerHTML = "";
  429. // if (!query) return console.log("no query");
  430. const origQuery = query;
  431. if (this._wordsDb) // search through word db
  432. {
  433. let q = query;
  434. const queryParts = [];
  435. let found = false;
  436. do
  437. {
  438. found = false;
  439. for (let i = 0; i < this._wordsDb.length; i++)
  440. {
  441. const idx = q.indexOf(this._wordsDb[i]);
  442. if (idx > -1) // && queryParts.indexOf(this._wordsDb[i])==-1
  443. {
  444. found = true;
  445. queryParts.push(this._wordsDb[i]);
  446. q = q.substr(0, idx) + " " + q.substr(idx + this._wordsDb[i].length, q.length - idx);
  447. break;
  448. }
  449. }
  450. }
  451. while (found);
  452. if (queryParts.length > 0)
  453. {
  454. let nquery = queryParts.join(" ");
  455. nquery += " " + q;
  456. if (nquery.trim() !== query) document.getElementById("realsearch").innerHTML = "Searching for: <b>" + nquery + "</b>";
  457. query = nquery;
  458. }
  459. else document.getElementById("realsearch").innerHTML = "";
  460. }
  461. else console.warn("no wordsdb");
  462. if (query.length > 1 && this._list)
  463. {
  464. for (let i = 0; i < this._list.length; i++)
  465. {
  466. this._list[i].score = 0;
  467. this._list[i].scoreDebug = "";
  468. }
  469. if (query.indexOf(" ") > -1)
  470. {
  471. const words = query.split(" ");
  472. for (let i = 0; i < words.length; i++) { this._searchWord(i, origQuery, this._list, words[i]); }
  473. }
  474. else
  475. {
  476. this._searchWord(0, query, this._list, originalSearch);
  477. }
  478. }
  479. }
  480. _rebuildWordList()
  481. {
  482. if (!this._list) return;
  483. const buildWordDB = {};
  484. for (let i = 0; i < this._list.length; i++)
  485. {
  486. const res = this._list[i].name.split(/(?=[A-Z,0-9,/.])/);
  487. for (let j = 0; j < res.length; j++)
  488. {
  489. if (res[j][res[j].length - 2] === "_") res[j] = res[j].substr(0, res[j].length - 2);
  490. if (res[j][0] === ".") res[j] = res[j].substr(1);
  491. if (res[j].length > 2) buildWordDB[res[j].toLowerCase()] = 1;
  492. }
  493. let shortName = "";
  494. const ccParts = this._list[i].shortName.split(/(?=[A-Z,0-9,/.])/);
  495. for (let j = 0; j < ccParts.length; j++)
  496. shortName += ccParts[j].substr(0, 1);
  497. this._list[i].abbrev = shortName.toLocaleLowerCase();
  498. }
  499. this._wordsDb = Object.keys(buildWordDB);
  500. this._wordsDb.sort((a, b) => { return b.length - a.length; });
  501. }
  502. _getOpsNamesFromCode(opnames, ns, val, parentname)
  503. {
  504. if (Object.prototype.toString.call(val) === "[object Object]")
  505. {
  506. for (const propertyName in val)
  507. {
  508. if (val.hasOwnProperty(propertyName))
  509. {
  510. const opName = ns + "." + parentname + propertyName;
  511. if (typeof (Patch.getOpClass(opName)) === "function") opnames.push(opName);
  512. opnames = this._getOpsNamesFromCode(opnames, ns, val[propertyName], parentname + propertyName + ".");
  513. }
  514. }
  515. }
  516. return opnames;
  517. }
  518. /**
  519. * @param {string[]} _opNames
  520. * @param {OpSearchListItem[]} [listItems]
  521. * @returns {OpSearchListItem[]}
  522. */
  523. _createListItemsByNames(_opNames, listItems = [])
  524. {
  525. if (!_opNames) return;
  526. const items = [];
  527. for (let i = 0; i < _opNames.length; i++)
  528. {
  529. const opName = _opNames[i];
  530. if (!opName) continue;
  531. const parts = opName.split(".");
  532. const lowerCaseName = opName.toLowerCase() + "_" + parts.join("").toLowerCase();
  533. const opDoc = gui.opDocs.getOpDocByName(opName);
  534. let shortName = opNames.getShortName(opName, true);
  535. let hidden = false;
  536. let opDocHidden = false;
  537. let opId = null;
  538. if (opDoc)
  539. {
  540. opId = opDoc.id;
  541. opDocHidden = opDoc.hidden;
  542. hidden = opDoc.hidden;
  543. shortName = opDoc.shortNameDisplay;
  544. }
  545. if (namespace.isDevOp(opName) && !platform.isDevEnv()) hidden = true;
  546. parts.length -= 1;
  547. const nameSpace = parts.join(".");
  548. if (namespace.isCollection(opName))
  549. {
  550. const inUse = listItems && listItems.some((op) => { return op.name.startsWith(opName); });
  551. if (inUse) hidden = true;
  552. }
  553. if (!hidden)
  554. {
  555. let oldState = "";
  556. if (hidden)oldState = "OLD";
  557. if (opDocHidden)oldState = "OLD";
  558. if (namespace.isDeprecatedOp(opName)) oldState = "DEPREC";
  559. let popularity = -1;
  560. let summary = gui.opDocs.getSummary(opName);
  561. let type = "op";
  562. if (namespace.isTeamNamespace(opName)) type = "team";
  563. if (namespace.isExtension(opName)) type = "extension";
  564. if (namespace.isPatchOp(opName)) type = "patchop";
  565. const isTeamOp = namespace.isTeamOp(opName);
  566. const isCollection = namespace.isCollection(opName);
  567. let collectionOpNames = null;
  568. if (isCollection)
  569. {
  570. const a = gui.opDocs.getNamespaceDocs(opName);
  571. if (a && a.length > 0 && a[0].ops) collectionOpNames = a[0].ops.join(" ").toLowerCase();
  572. }
  573. /** @type {OpSearchListItem} */
  574. const op = {
  575. "opId": opId || utils.simpleId(),
  576. "name": opName,
  577. "summary": summary,
  578. "collectionOpNames": collectionOpNames,
  579. "nscolor": opNames.getNamespaceClassName(opName),
  580. "isOp": !namespace.isCollection(opName),
  581. "userOp": namespace.isUserOp(opName),
  582. "devOp": namespace.isDevOp(opName),
  583. "extensionOp": namespace.isExtensionOp(opName),
  584. "teamOp": namespace.isTeamOp(opName),
  585. "patchOp": namespace.isPatchOp(opName),
  586. "isExtension": namespace.isExtension(opName),
  587. "isTeamNamespace": isTeamOp,
  588. "shortName": shortName,
  589. "nameSpace": nameSpace,
  590. "oldState": oldState,
  591. "lowercasename": lowerCaseName,
  592. "isCollection": isCollection,
  593. "buttonText": isCollection ? "Load" : "Add",
  594. "type": type,
  595. "pop": popularity,
  596. "namespaces": []
  597. };
  598. if (namespace.isCollection(opName))
  599. {
  600. op.isOp = false;
  601. op.pop = 1;
  602. if (opDoc)
  603. {
  604. op.summary = opDoc.summary;
  605. op.description = opDoc.description;
  606. op.teamName = opDoc.teamName;
  607. op.teamLink = opDoc.teamLink;
  608. op.numOps = opDoc.numOps;
  609. op.ops = opDoc.ops || [];
  610. }
  611. }
  612. let ns = "Ops";
  613. for (let j = 1; j < parts.length; j++)
  614. {
  615. ns += "." + parts[j];
  616. op.namespaces.push(ns);
  617. }
  618. items.push(op);
  619. }
  620. }
  621. return items;
  622. }
  623. }