@datagrok/hit-triage 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/hit-triage",
3
3
  "friendlyName": "HitTriage",
4
- "version": "1.3.2",
4
+ "version": "1.3.4",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
@@ -227,5 +227,6 @@ export function getTileCategoryEditor(preset?: string[]) {
227
227
  return {
228
228
  getFields: getFieldParams,
229
229
  fieldsDiv: itemsGrid.root,
230
+ itemsGrid,
230
231
  };
231
232
  }
package/src/app/consts.ts CHANGED
@@ -17,17 +17,17 @@ export const HTScriptPrefix = 'HTScript';
17
17
  export const HTQueryPrefix = 'HTQuery';
18
18
  export const ComputeQueryMolColName = 'molecules';
19
19
  export const i18n = {
20
- startNewCampaign: 'New campaign',
21
- createNewCampaign: 'New campaign',
20
+ startNewCampaign: 'New Campaign',
21
+ createNewCampaign: 'New Campaign',
22
22
  dataSourceFunction: 'Source',
23
- createNewTemplate: 'New template',
23
+ createNewTemplate: 'New Template',
24
24
  StartCampaign: 'Start',
25
25
  createTemplate: 'Create',
26
26
  createCampaign: 'Create',
27
27
  download: 'Download',
28
28
  cancel: 'Cancel',
29
- continueCampaigns: 'Continue a campaign',
30
- createNewCampaignHeader: 'New campaign',
29
+ continueCampaigns: 'Continue Campaign',
30
+ createNewCampaignHeader: 'New Campaign',
31
31
  selectTemplate: 'Template',
32
32
  } as const;
33
33
 
@@ -36,3 +36,11 @@ export const funcTypeNames = {
36
36
  function: 'function-package',
37
37
  query: 'data-query',
38
38
  } as const;
39
+
40
+ export const HDCampaignsGroupingLSKey = 'HDCampaignsGrouping';
41
+
42
+ export enum CampaignGroupingType {
43
+ None = 'None',
44
+ Template = 'Template',
45
+ Status = 'Status',
46
+ }
@@ -1,7 +1,8 @@
1
+ /* eslint-disable max-len */
1
2
  import * as grok from 'datagrok-api/grok';
2
3
  import * as ui from 'datagrok-api/ui';
3
4
  import * as DG from 'datagrok-api/dg';
4
- import {AppName, HitDesignCampaign, HitDesignTemplate, HitTriageCampaignStatus, IFunctionArgs} from './types';
5
+ import {AppName, HitDesignCampaign, HitDesignTemplate, IFunctionArgs} from './types';
5
6
  import {HitDesignInfoView} from './hit-design-views/info-view';
6
7
  import {CampaignIdKey, CampaignJsonName, CampaignTableName,
7
8
  HTQueryPrefix, HTScriptPrefix, HitDesignCampaignIdKey,
@@ -35,6 +36,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
35
36
  protected currentDesignViewId?: string;
36
37
  public mainView: DG.ViewBase;
37
38
  protected get version() {return this._campaign?.version ?? 0;};
39
+ public existingStatuses: string[] = [];
38
40
  constructor(c: DG.FuncCall, an: AppName = 'Hit Design',
39
41
  infoViewConstructor: (app: HitDesignApp) => HitDesignInfoView = (app) => new HitDesignInfoView(app)) {
40
42
  super(c, an);
@@ -72,6 +74,48 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
72
74
  }));
73
75
  }
74
76
 
77
+ public get stages() {
78
+ return this.campaign?.template?.stages ?? this.template?.stages ?? [];
79
+ }
80
+
81
+ public async setStages(st: string[]) {
82
+ if (!this.campaign || !this.campaign.template || !this.template) {
83
+ grok.shell.error('Campaign or template is not set');
84
+ return;
85
+ }
86
+ if (!st?.length) {
87
+ grok.shell.error('Removing all stages is not allowed');
88
+ return;
89
+ }
90
+ const stageCol = this.dataFrame?.col(TileCategoriesColName);
91
+ if (!stageCol) {
92
+ grok.shell.error('No stage column found');
93
+ return;
94
+ }
95
+ const removedStages: string[] = [];
96
+ //make sure there is no duplication
97
+ const stageSet = new Set(st);
98
+ const uniqueStages = [...stageSet];
99
+ const dfLen = this.dataFrame!.rowCount;
100
+ const stageCats = stageCol.categories;
101
+ const stageIndexes = stageCol.getRawData() as Int32Array;
102
+ for (let i = 0; i < dfLen; i++) {
103
+ const stage = stageCats[stageIndexes[i]];
104
+ if (!stageSet.has(stage)) {
105
+ stageCol.set(i, st[0], false);
106
+ removedStages.push(stage);
107
+ }
108
+ }
109
+
110
+ if (removedStages.length > 0)
111
+ grok.shell.warning(`Some stages were removed: (${removedStages.join(', ')}). Corresponding rows were set to stage "${st[0]}"`);
112
+
113
+
114
+ this.campaign.template.stages = uniqueStages;
115
+ this.template.stages = uniqueStages;
116
+ await this.saveCampaign(true);
117
+ }
118
+
75
119
  public async setTemplate(template: T, campaignId?: string) {
76
120
  if (!campaignId) {
77
121
  this._designView?.dataFrame && grok.shell.closeTable(this._designView.dataFrame);
@@ -183,7 +227,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
183
227
  this.dataFrame!.col(col.name)!.set(newValueIdx, col.get(0), false);
184
228
  }
185
229
  this.dataFrame!.fireValuesChanged();
186
- this.saveCampaign(undefined, false);
230
+ this.saveCampaign(false);
187
231
  }
188
232
 
189
233
  protected initDesignViewRibbons(view: DG.TableView, subs: Subscription[]) {
@@ -275,7 +319,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
275
319
  this.dataFrame!.fireValuesChanged();
276
320
  } finally {
277
321
  ui.setUpdateIndicator(view.grid.root, false);
278
- this.saveCampaign(undefined, false);
322
+ this.saveCampaign(false);
279
323
  }
280
324
  }, () => null, this.campaign?.template!, true);
281
325
  };
@@ -285,7 +329,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
285
329
  const permissionsButton = ui.iconFA('share', async () => {
286
330
  await (new PermissionsDialog(this.campaign?.permissions)).show((res) => {
287
331
  this.campaign!.permissions = res;
288
- this.saveCampaign(undefined, true);
332
+ this.saveCampaign(true);
289
333
  });
290
334
  }, 'Edit campaign permissions');
291
335
  const tilesButton = ui.bigButton('Progress tracker', () => {
@@ -297,15 +341,21 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
297
341
  if (dialogContent) {
298
342
  const dlg = ui.dialog('Submit');
299
343
  dlg.add(dialogContent);
300
- dlg.addButton('Save', ()=>{this.saveCampaign(); dlg.close();});
301
- dlg.addButton('Submit', ()=>{this._submitView?.submit(); dlg.close();});
344
+ dlg.addButton('Save', () => {
345
+ this._campaign!.status = this._submitView!.getStatus();
346
+ this.saveCampaign();
347
+ dlg.close();
348
+ });
349
+ if (this.template?.submit?.fName && this.template?.submit?.package && DG.Func.find({name: this.template.submit.fName, package: this.template.submit.package})?.length > 0)
350
+ dlg.addButton('Submit', ()=>{this._submitView?.submit(); dlg.close();});
302
351
  dlg.show();
303
352
  }
304
353
  });
305
354
  submitButton.classList.add('hit-design-submit-button');
306
355
  const ribbonButtons: HTMLElement[] = [submitButton];
307
- if (this.template?.stages?.length ?? 0 > 0)
356
+ if (this.stages.length > 0)
308
357
  ribbonButtons.unshift(tilesButton);
358
+ // only initialize campaign template if its not exsitent yet
309
359
  if (this.campaign && this.template && !this.campaign.template)
310
360
  this.campaign.template = this.template;
311
361
 
@@ -358,11 +408,11 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
358
408
  subs.push(this.dataFrame!.onRowsAdded.pipe(filter(() => !this.isJoining))
359
409
  .subscribe(() => { // TODO, insertion of rows in the middle
360
410
  try {
361
- if (this.template!.stages?.length > 0) {
411
+ if (this.stages.length > 0) {
362
412
  for (let i = 0; i < this.dataFrame!.rowCount; i++) {
363
413
  const colVal = this.dataFrame!.col(TileCategoriesColName)!.get(i);
364
414
  if (!colVal || colVal === '' || this.dataFrame!.col(TileCategoriesColName)?.isNone(i))
365
- this.dataFrame!.set(TileCategoriesColName, i, this.template!.stages[0]);
415
+ this.dataFrame!.set(TileCategoriesColName, i, this.stages[0]);
366
416
  }
367
417
  }
368
418
  let lastAddedCell: DG.GridCell | null = null;
@@ -432,7 +482,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
432
482
  view?.grid && subs.push(view.grid.onCellValueEdited.subscribe(async (gc) => {
433
483
  try {
434
484
  if (gc.tableColumn?.name === TileCategoriesColName) {
435
- await this.saveCampaign(undefined, false);
485
+ await this.saveCampaign(false);
436
486
  return;
437
487
  }
438
488
  if (gc.tableColumn?.name !== this.molColName)
@@ -566,7 +616,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
566
616
 
567
617
  if (this._campaign)
568
618
  this._campaign!.savePath = this._filePath;
569
- await this.saveCampaign(undefined, true);
619
+ await this.saveCampaign(true);
570
620
  ui.empty(pathDiv);
571
621
  const folderPath = getFolderPath();
572
622
  link = ui.link(folderPath,
@@ -603,7 +653,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
603
653
  };
604
654
  }
605
655
 
606
- async saveCampaign(status?: HitTriageCampaignStatus, notify = true): Promise<HitDesignCampaign> {
656
+ async saveCampaign(notify = true): Promise<HitDesignCampaign> {
607
657
  const campaignId = this.campaignId!;
608
658
  const templateName = this.template!.name;
609
659
  const enrichedDf = this.dataFrame!;
@@ -621,7 +671,7 @@ export class HitDesignApp<T extends HitDesignTemplate = HitDesignTemplate> exten
621
671
  const campaign: HitDesignCampaign = {
622
672
  name: campaignName,
623
673
  templateName,
624
- status: status ?? this.campaign?.status ?? 'In Progress',
674
+ status: this.campaign?.status ?? 'In Progress',
625
675
  createDate: this.campaign?.createDate ?? toFormatedDateString(new Date()),
626
676
  campaignFields: this.campaign?.campaignFields ?? this.campaignProps,
627
677
  columnSemTypes,
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-len */
1
2
  import * as grok from 'datagrok-api/grok';
2
3
  import * as ui from 'datagrok-api/ui';
3
4
  import * as DG from 'datagrok-api/dg';
@@ -5,10 +6,13 @@ import {u2} from '@datagrok-libraries/utils/src/u2';
5
6
  import {HitDesignApp} from '../hit-design-app';
6
7
  import {_package} from '../../package';
7
8
  import $ from 'cash-dom';
8
- import {CampaignJsonName, HitDesignCampaignIdKey, i18n} from '../consts';
9
+ import {CampaignGroupingType, CampaignJsonName, HitDesignCampaignIdKey, i18n} from '../consts';
9
10
  import {HitDesignCampaign, HitDesignTemplate} from '../types';
10
11
  import {addBreadCrumbsToRibbons, checkEditPermissions,
11
- checkViewPermissions, loadCampaigns, modifyUrl, popRibbonPannels} from '../utils';
12
+ checkViewPermissions, getGroupedCampaigns, getSavedCampaignsGrouping,
13
+ loadCampaigns, modifyUrl, popRibbonPannels,
14
+ processGroupingTable,
15
+ setSavedCampaignsGrouping} from '../utils';
12
16
  import {newHitDesignCampaignAccordeon} from '../accordeons/new-hit-design-campaign-accordeon';
13
17
  import {newHitDesignTemplateAccordeon} from '../accordeons/new-hit-design-template-accordeon';
14
18
  import {HitBaseView} from '../base-view';
@@ -17,6 +21,7 @@ import {defaultPermissions, PermissionsDialog} from '../dialogs/permissions-dial
17
21
  export class HitDesignInfoView
18
22
  <T extends HitDesignTemplate = HitDesignTemplate, K extends HitDesignApp = HitDesignApp>
19
23
  extends HitBaseView<T, K> {
24
+ currentSorting: string = 'None';
20
25
  constructor(app: K) {
21
26
  super(app);
22
27
  this.name = 'Hit Design';
@@ -48,6 +53,7 @@ export class HitDesignInfoView
48
53
  ui.setUpdateIndicator(this.root, true);
49
54
  try {
50
55
  const continueCampaignsHeader = ui.h1(i18n.continueCampaigns);
56
+
51
57
  const createNewCampaignHeader = ui.h1(i18n.createNewCampaignHeader, {style: {marginLeft: '10px'}});
52
58
  const appHeader = this.getAppHeader();
53
59
 
@@ -56,10 +62,38 @@ export class HitDesignInfoView
56
62
  const contentDiv = ui.div([templatesDiv, campaignAccordionDiv], 'ui-form');
57
63
 
58
64
  const campaignsTable = await this.getCampaignsTable();
65
+ const tableRoot = ui.div([campaignsTable], {style: {position: 'relative'}});
66
+
67
+ const sortIcon = ui.iconFA('sort', () => {
68
+ const menu = DG.Menu.popup();
69
+ Object.values(CampaignGroupingType).forEach((i) => {
70
+ menu.item(i, async () => {
71
+ setSavedCampaignsGrouping(i as CampaignGroupingType);
72
+ ui.setUpdateIndicator(tableRoot, true);
73
+ try {
74
+ const t = await this.getCampaignsTable();
75
+ ui.setUpdateIndicator(tableRoot, false);
76
+ ui.empty(tableRoot);
77
+ tableRoot.appendChild(t);
78
+ } catch (e) {
79
+ grok.shell.error('Failed to update campaigns table');
80
+ console.error(e);
81
+ } finally {
82
+ ui.setUpdateIndicator(tableRoot, false);
83
+ }
84
+ });
85
+ menu.show({element: sortingHeader, x: 100, y: sortingHeader.offsetTop + 30});
86
+ });
87
+ });
88
+ sortIcon.style.marginBottom = '12px';
89
+ sortIcon.style.marginLeft = '5px';
90
+ sortIcon.style.fontSize = '15px';
91
+ ui.tooltip.bind(sortIcon, () => `Group Campaigns. Current: ${this.currentSorting}`);
92
+ const sortingHeader = ui.divH([continueCampaignsHeader, sortIcon], {style: {alignItems: 'center'}});
59
93
  $(this.root).empty();
60
94
  this.root.appendChild(ui.div([
61
- ui.divV([appHeader, continueCampaignsHeader], {style: {marginLeft: '10px'}}),
62
- campaignsTable,
95
+ ui.divV([appHeader, sortingHeader], {style: {marginLeft: '10px'}}),
96
+ tableRoot,
63
97
  createNewCampaignHeader,
64
98
  contentDiv,
65
99
  ]));
@@ -178,7 +212,10 @@ export class HitDesignInfoView
178
212
 
179
213
  private async getCampaignsTable() {
180
214
  const campaignNamesMap = await loadCampaigns(this.app.appName, this.deletedCampaigns);
181
-
215
+ const grouppingMode = getSavedCampaignsGrouping();
216
+ const grouppedCampaigns = getGroupedCampaigns<HitDesignCampaign>(Object.values(campaignNamesMap), grouppingMode);
217
+ this.currentSorting = grouppingMode;
218
+ this.app.existingStatuses = Array.from(new Set(Object.values(campaignNamesMap).map((c) => c.status).filter((s) => !!s)));
182
219
  const deleteAndShareCampaignIcons = (info: HitDesignCampaign) => {
183
220
  const deleteIcon = ui.icons.delete(async () => {
184
221
  ui.dialog('Delete campaign')
@@ -226,6 +263,7 @@ export class HitDesignInfoView
226
263
  ['Name', 'Created', 'Molecules', 'Status', '']);
227
264
  table.style.color = 'var(--grey-5)';
228
265
  table.style.marginLeft = '24px';
266
+ processGroupingTable(table, grouppedCampaigns);
229
267
  return table;
230
268
  }
231
269
 
@@ -239,7 +277,7 @@ export class HitDesignInfoView
239
277
  this.app.dataFrame = camp.df;
240
278
  await this.app.setTemplate(template);
241
279
  this.app.campaignProps = camp.campaignProps;
242
- await this.app.saveCampaign(undefined, false);
280
+ await this.app.saveCampaign(false);
243
281
  if (template.layoutViewState && this.app.campaign)
244
282
  this.app.campaign.layout = template.layoutViewState;
245
283
  });
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-len */
1
2
  import * as ui from 'datagrok-api/ui';
2
3
  import * as grok from 'datagrok-api/grok';
3
4
  import * as DG from 'datagrok-api/dg';
@@ -7,19 +8,54 @@ import {HitDesignTemplate} from '../types';
7
8
  import {HitBaseView} from '../base-view';
8
9
 
9
10
  export class HitDesignSubmitView extends HitBaseView<HitDesignTemplate, HitDesignApp> {
11
+ private statusInput: DG.InputBase<string | undefined>;
12
+ private statusSuggestionsMenu: DG.Menu;
13
+ content: HTMLDivElement;
10
14
  constructor(app: HitDesignApp) {
11
15
  super(app);
12
16
  this.name = 'Submit';
17
+ this.statusInput = ui.input.string('Status', {value: this.app.campaign?.status, nullable: false});
18
+ this.statusSuggestionsMenu = DG.Menu.popup();
19
+ this.statusInput.root.style.marginLeft = '12px';
20
+ this.content = ui.div();
21
+
22
+ this.statusInput.onChanged.subscribe(() => {
23
+ this.statusSuggestionsMenu.clear();
24
+ const status = (this.statusInput.value ?? '').toLowerCase();
25
+ // eslint-disable-next-line max-len
26
+ const similarStatuses = this.app.existingStatuses.filter((s) => s?.toLowerCase()?.includes(status) && s?.toLowerCase() !== status).filter((_, i) => i < 5);
27
+ if (similarStatuses.length) {
28
+ similarStatuses.forEach((s) => {
29
+ this.statusSuggestionsMenu.item(s, () => {
30
+ this.statusInput.value = s;
31
+ this.statusSuggestionsMenu.root.remove();
32
+ this.statusInput.root.focus();
33
+ });
34
+ });
35
+ if (this.content.parentElement?.parentElement) {
36
+ const xOffset = this.statusInput.root.offsetLeft + (this.statusInput.input?.offsetLeft ?? 0);
37
+ const yOffset = this.statusInput.root.offsetTop + this.statusInput.root.offsetHeight + this.content.parentElement.offsetTop;
38
+ this.statusSuggestionsMenu.show({element: this.content.parentElement.parentElement!, x: xOffset, y: yOffset});
39
+ }
40
+ } else
41
+ this.statusSuggestionsMenu.root.remove();
42
+ });
43
+ }
44
+
45
+ public getStatus() {
46
+ return this.statusInput.value ?? this.app.campaign?.status ?? 'No Status';
13
47
  }
14
48
 
15
49
  render(): HTMLDivElement {
16
- ui.empty(this.root);
50
+ this.statusInput.value = this.app.campaign?.status ?? '';
51
+ ui.empty(this.content);
17
52
 
18
- const content = ui.divV([
53
+ this.content = ui.divV([
19
54
  ui.h1('Summary'),
20
55
  ui.div([ui.tableFromMap(this.app.getSummary())]),
56
+ this.statusInput.root,
21
57
  ]);
22
- return content;
58
+ return this.content;
23
59
  }
24
60
 
25
61
  onActivated(): void {
@@ -37,8 +73,8 @@ export class HitDesignSubmitView extends HitBaseView<HitDesignTemplate, HitDesig
37
73
  }
38
74
  const filteredDf = DG.DataFrame.fromCsv(this.app.dataFrame!.toCsv({filteredRowsOnly: true}));
39
75
  await submitFn.apply({df: filteredDf, molecules: this.app.molColName});
40
- this.app.campaign && (this.app.campaign.status = 'Submitted');
41
- this.app.saveCampaign('Submitted');
76
+ this.app.campaign!.status = this.getStatus();
77
+ this.app.saveCampaign();
42
78
  grok.shell.info('Submitted successfully.');
43
79
  }
44
80
  }
@@ -5,6 +5,7 @@ import {HitDesignApp} from '../hit-design-app';
5
5
  import {_package} from '../../package';
6
6
  import {TileCategoriesColName} from '../consts';
7
7
  import './utils.css';
8
+ import {getTileCategoryEditor} from '../accordeons/new-hit-design-template-accordeon';
8
9
 
9
10
  export function getTilesViewDialog(app: HitDesignApp, getTableView: () => DG.TableView | null) {
10
11
  const tilesViewerSketchStateString = app.campaign?.tilesViewerFormSketch;
@@ -32,7 +33,8 @@ export function getTilesViewDialog(app: HitDesignApp, getTableView: () => DG.Tab
32
33
  }
33
34
  }
34
35
 
35
- const tileOpts = {lanesColumnName: TileCategoriesColName, lanes: app.template?.stages ?? [],
36
+ const tileOpts = {lanesColumnName: TileCategoriesColName,
37
+ lanes: app.stages,
36
38
  ...((sketchState?.elementStates?.length ?? 0) > 0 ? {sketchState} : {})};
37
39
 
38
40
  const tv = getTableView();
@@ -59,15 +61,50 @@ export function getTilesViewDialog(app: HitDesignApp, getTableView: () => DG.Tab
59
61
  v.detach();
60
62
  }
61
63
  v = tv.addViewer(DG.VIEWER.TILE_VIEWER,
62
- {lanesColumnName: TileCategoriesColName, lanes: app.template?.stages ?? []});
64
+ {lanesColumnName: TileCategoriesColName, lanes: app.stages});
63
65
  }
64
66
 
65
67
  if (!v) {
66
68
  grok.shell.error('Failed to create tiles viewer. check the console for more details.');
67
69
  return;
68
70
  }
69
- modal.add(v.root);
70
71
 
72
+ let stageEditorDialog: DG.Dialog | null = null;
73
+ const closeViewer = () => {// it can be already closed by the time we get here
74
+ try {
75
+ v.detach();
76
+ v.close();
77
+ } catch (e) {
78
+ }
79
+ };
80
+ modal.add(v.root);
81
+ modal.addButton('Modify Stages', () => {
82
+ stageEditorDialog?.close();
83
+ const stageEditor = getTileCategoryEditor(app.stages);
84
+ stageEditorDialog = ui.dialog('Modify Stages')
85
+ .add(stageEditor.fieldsDiv)
86
+ .onOK(async () => {
87
+ closeViewer(); // so that its not included in the layout.
88
+ ui.setUpdateIndicator(modal.root, true);
89
+ try {
90
+ await app.setStages(stageEditor.getFields());
91
+ ui.setUpdateIndicator(modal.root, false);
92
+ modal.close();
93
+ await new Promise<void>((r) => setTimeout(() => {
94
+ r();
95
+ getTilesViewDialog(app, getTableView);
96
+ }, 100));
97
+ } catch (e) {
98
+ grok.shell.error('Failed to update stages. check the console for more details.');
99
+ console.error('Failed to update stages', e);
100
+ } finally {
101
+ if (modal?.root && document.contains(modal.root))
102
+ modal.close();
103
+ }
104
+ })
105
+ .show();
106
+ stageEditorDialog.root.classList.add('hit-design-stage-editing-dialog');
107
+ }, 0);
71
108
  // from this modal, only way to go to another view is through edit form.
72
109
  // when this happens, we should not destroy the viewer,
73
110
  //but just hide it so that when we come back, we can show it again.
@@ -85,6 +122,7 @@ export function getTilesViewDialog(app: HitDesignApp, getTableView: () => DG.Tab
85
122
  const closeSub = modal.onClose.subscribe(() => {
86
123
  closeSub.unsubscribe();
87
124
  viewChangeSub.unsubscribe();
125
+ stageEditorDialog?.close();
88
126
  // save the sketch state
89
127
  try {
90
128
  const sketchState = v.props.sketchState;
@@ -97,13 +135,13 @@ export function getTilesViewDialog(app: HitDesignApp, getTableView: () => DG.Tab
97
135
  grok.shell.error('Failed to save sketch state. check the console for more details.');
98
136
  console.error('Failed to save sketch state', e);
99
137
  }
100
- v.detach();
101
- v.close();
138
+ closeViewer();
102
139
  });
103
140
 
104
141
  modal.showModal(true);
105
142
 
106
-
143
+ modal.getButton('CANCEL')?.remove();
144
+ modal.onOK(() => {});
107
145
  // remove the modal background
108
146
  document.querySelector('.d4-modal-background')?.remove();
109
147
  }
@@ -1,3 +1,11 @@
1
1
  img[alt='hitDesignReadmeImg'] {
2
2
  max-width: 100%;
3
+ }
4
+
5
+ .hit-design-stage-editing-dialog {
6
+ z-index: 10000 !important;
7
+ }
8
+
9
+ .hit-design-groupped-campaigns-table .d4-accordion-pane-content.expanded {
10
+ margin: 0 !important;
3
11
  }
@@ -80,11 +80,11 @@ export class PeptiHitApp extends HitDesignApp<PeptiHitTemplate> {
80
80
  subs.push(this.dataFrame!.onRowsAdded.pipe(filter(() => !this.isJoining))
81
81
  .subscribe(() => { // TODO, insertion of rows in the middle
82
82
  try {
83
- if (this.template!.stages?.length > 0) {
83
+ if (this.stages.length > 0) {
84
84
  for (let i = 0; i < this.dataFrame!.rowCount; i++) {
85
85
  const colVal = this.dataFrame!.col(TileCategoriesColName)!.get(i);
86
86
  if (!colVal || colVal === '' || this.dataFrame!.col(TileCategoriesColName)?.isNone(i))
87
- this.dataFrame!.set(TileCategoriesColName, i, this.template!.stages[0]);
87
+ this.dataFrame!.set(TileCategoriesColName, i, this.stages[0]);
88
88
  }
89
89
  }
90
90
  let lastAddedCell: DG.GridCell | null = null;
@@ -154,7 +154,7 @@ export class PeptiHitApp extends HitDesignApp<PeptiHitTemplate> {
154
154
  view?.grid && subs.push(view.grid.onCellValueEdited.subscribe(async (gc) => {
155
155
  try {
156
156
  if (gc.tableColumn?.name === TileCategoriesColName) {
157
- await this.saveCampaign(undefined, false);
157
+ await this.saveCampaign(false);
158
158
  return;
159
159
  }
160
160
  if (gc.tableColumn?.name !== this.helmColName)
@@ -22,7 +22,7 @@ export class PeptiHitInfoView extends HitDesignInfoView<PeptiHitTemplate, PeptiH
22
22
  this.app.dataFrame = camp.df;
23
23
  await this.app.setTemplate(template);
24
24
  this.app.campaignProps = camp.campaignProps;
25
- await this.app.saveCampaign(undefined, false);
25
+ await this.app.saveCampaign(false);
26
26
  if (template.layoutViewState && this.app.campaign)
27
27
  this.app.campaign.layout = template.layoutViewState;
28
28
  });
package/src/app/types.ts CHANGED
@@ -100,7 +100,7 @@ export type HitTriageTemplateSubmit = {
100
100
  package: string
101
101
  };
102
102
 
103
- export type HitTriageCampaignStatus = 'In Progress' | 'Submitted';
103
+ export type HitTriageCampaignStatus = string;
104
104
 
105
105
  export type HitTriageCampaign = {
106
106
  name: string,
package/src/app/utils.ts CHANGED
@@ -1,9 +1,10 @@
1
+ /* eslint-disable max-len */
1
2
  import * as grok from 'datagrok-api/grok';
2
3
  import * as ui from 'datagrok-api/ui';
3
4
  import * as DG from 'datagrok-api/dg';
4
5
  import {Subscription} from 'rxjs';
5
- import {CampaignJsonName, ComputeQueryMolColName} from './consts';
6
- import {AppName, CampaignsType, TriagePermissions} from './types';
6
+ import {CampaignGroupingType, CampaignJsonName, ComputeQueryMolColName, HDCampaignsGroupingLSKey} from './consts';
7
+ import {AppName, CampaignsType, HitDesignCampaign, HitTriageCampaign, TriagePermissions} from './types';
7
8
  import {_package} from '../package';
8
9
 
9
10
  export const toFormatedDateString = (d: Date): string => {
@@ -164,3 +165,82 @@ export async function checkEditPermissions(authorId: string, permissions: Triage
164
165
  export async function checkViewPermissions(authorId: string, permissions: TriagePermissions): Promise<boolean> {
165
166
  return checkPermissions(authorId, Array.from(new Set([...permissions.view, ...permissions.edit])));
166
167
  }
168
+
169
+ export function getLocalStorageValue<T = string>(key: string): T | null {
170
+ return localStorage.getItem(key) as unknown as T;
171
+ }
172
+
173
+ export function setLocalStorageValue(key: string, value: string) {
174
+ localStorage.setItem(key, value as unknown as string);
175
+ }
176
+
177
+ export function getSavedCampaignsGrouping(): CampaignGroupingType {
178
+ return getLocalStorageValue<CampaignGroupingType>(HDCampaignsGroupingLSKey) ?? CampaignGroupingType.None;
179
+ }
180
+
181
+ export function setSavedCampaignsGrouping(value: CampaignGroupingType) {
182
+ setLocalStorageValue(HDCampaignsGroupingLSKey, value);
183
+ }
184
+
185
+ export const getGroupingKey = <T extends HitDesignCampaign | HitTriageCampaign = HitDesignCampaign>(grouping: CampaignGroupingType, campaign: T): string => {
186
+ switch (grouping) {
187
+ case CampaignGroupingType.Template:
188
+ return campaign.template?.key ?? campaign.templateName;
189
+ case CampaignGroupingType.Status:
190
+ return campaign.status;
191
+ default:
192
+ return '';
193
+ }
194
+ };
195
+
196
+ export function getGroupedCampaigns<T extends HitDesignCampaign | HitTriageCampaign = HitDesignCampaign>(campaigns: T[], grouping: CampaignGroupingType):
197
+ {[key: string]: T[]} {
198
+ if (grouping === CampaignGroupingType.None)
199
+ return {'': campaigns};
200
+ const groupedCampaigns: {[key: string]: T[]} = {};
201
+ for (const campaign of campaigns) {
202
+ const key = getGroupingKey(grouping, campaign);
203
+ if (!groupedCampaigns[key])
204
+ groupedCampaigns[key] = [];
205
+ groupedCampaigns[key].push(campaign);
206
+ }
207
+ return groupedCampaigns;
208
+ }
209
+
210
+ export function processGroupingTable<T extends HitDesignCampaign | HitTriageCampaign = HitDesignCampaign>(table: HTMLTableElement, groupedCampaigns: {[key: string]: T[]}, numCols = 6) {
211
+ table.classList.add('hit-design-groupped-campaigns-table');
212
+ const keys = Object.keys(groupedCampaigns);
213
+ if (keys.length < 2)
214
+ return table;
215
+ const body = table.getElementsByTagName('tbody')[0];
216
+ const rows = Array.from(table.getElementsByTagName('tr')).filter((row) => !row.classList.contains('header'));
217
+ let curRow = 0;
218
+
219
+ const setState = (expanded: boolean, start: number, end: number) => {
220
+ for (let i = start; i < end; i++)
221
+ rows[i]?.style && (rows[i].style.display = expanded ? 'table-row' : 'none');
222
+ };
223
+
224
+
225
+ for (const key of keys) {
226
+ const row = rows[curRow];
227
+ if (!row)
228
+ break;
229
+ const l = groupedCampaigns[key].length;
230
+ const startRow = curRow;
231
+ const endRow = curRow + l;
232
+ const acc = ui.accordion(`Hit-Design-campaigns-group-${key}`);
233
+ const pane = acc.addPane(key, () => {return ui.div();}, undefined, undefined, false);
234
+ pane.root.style.marginLeft = '-24px';
235
+ pane.root.onclick = () => {
236
+ setState(pane.expanded, startRow, endRow);
237
+ };
238
+ setState(pane.expanded, startRow, endRow);
239
+ const newRow = body.insertRow(0);
240
+ const newCell = newRow.insertCell(0);
241
+ newCell.appendChild(pane.root);
242
+ newCell.colSpan = numCols;
243
+ body.insertBefore(newRow, row);
244
+ curRow += l;
245
+ }
246
+ }