fraim-hub 2.0.262 → 2.0.263

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,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.262",
3
+ "version": "2.0.263",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "bin": {
6
6
  "fraim-hub": "bin/fraim-hub.js",
@@ -163,7 +163,7 @@
163
163
  "electron": "^41.2.2",
164
164
  "electron-updater": "^6.8.9",
165
165
  "express": "^5.2.1",
166
- "fraim": "2.0.262",
166
+ "fraim": "2.0.263",
167
167
  "mongodb": "^7.0.0",
168
168
  "node-cron": "4.2.1",
169
169
  "node-edge-tts": "^1.2.10",
@@ -1132,6 +1132,39 @@
1132
1132
  </div>
1133
1133
 
1134
1134
  <!-- Issue #834: Sharing-backend chooser modal -->
1135
+ <!-- Share destination modal: shown when the user clicks "Share with others" -->
1136
+ <div class="modal-backdrop" id="share-dest-modal" role="dialog" aria-modal="true" aria-labelledby="share-dest-title" hidden>
1137
+ <div class="modal">
1138
+ <div class="modal-header">
1139
+ <h2 id="share-dest-title">Share learnings</h2>
1140
+ <p id="share-dest-sub">Choose where to share these learnings.</p>
1141
+ </div>
1142
+ <div class="modal-body">
1143
+ <div class="lp-pp-dest-row" id="share-dest-row">
1144
+ <button class="lp-pp-dest-btn" type="button" data-dest="org" id="share-dest-org">
1145
+ <span class="lp-pp-dest-name">Company</span>
1146
+ <span class="lp-pp-dest-sub">Everyone in the organization</span>
1147
+ </button>
1148
+ <button class="lp-pp-dest-btn" type="button" data-dest="project" id="share-dest-proj">
1149
+ <span class="lp-pp-dest-name">This project</span>
1150
+ <span class="lp-pp-dest-sub">Visible on a specific project</span>
1151
+ </button>
1152
+ </div>
1153
+ <div class="lp-pp-project-section" id="share-dest-proj-section" hidden>
1154
+ <label class="lp-pp-label" for="share-dest-proj-sel">Which project?</label>
1155
+ <select class="lp-pp-sel" id="share-dest-proj-sel"></select>
1156
+ </div>
1157
+ </div>
1158
+ <div class="modal-footer">
1159
+ <span class="left" id="share-dest-hint"></span>
1160
+ <div class="right">
1161
+ <button class="ghost" type="button" id="share-dest-cancel">Cancel</button>
1162
+ <button class="send-button" type="button" id="share-dest-go" disabled>Start</button>
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ </div>
1167
+
1135
1168
  <div class="modal-backdrop" id="sharing-chooser-modal" role="dialog" aria-modal="true" aria-labelledby="sharing-chooser-title" hidden>
1136
1169
  <div class="modal">
1137
1170
  <div class="modal-header">
@@ -13157,114 +13157,93 @@ async function tfRunShareWithOthers(scope, level, selectedTitles) {
13157
13157
  const job = templates.find((t) => t.id === slug)
13158
13158
  || jobs.find((j) => j.id === slug)
13159
13159
  || { id: slug, title: 'Share with others', intent: 'Move artifacts to the right scope level.' };
13160
- if (!state.projectPath) {
13161
- if (typeof showStatus === 'function') showStatus('Pick a project folder first, then share learnings.', true);
13162
- return;
13163
- }
13164
- // Always show the destination picker when sharing from the Manager tab —
13165
- // the manager must choose Company vs. This project regardless of how many
13166
- // projects are loaded.
13167
- const hostId = scope === 'manager' && level === 'machine' ? 'manager-learnings' : 'project-learnings';
13168
- tfShareDestinationPicker(hostId, scope, level, selectedTitles, job);
13160
+ tfShareDestinationPicker(scope, level, selectedTitles, job);
13169
13161
  }
13170
13162
 
13171
- // Destination picker: first ask Company vs. This project, then (for project)
13172
- // optionally ask which project when more than one is loaded.
13173
- function tfShareDestinationPicker(hostId, scope, level, selectedTitles, job) {
13174
- const host = document.getElementById(hostId);
13175
- if (!host) return;
13176
- // Remove any existing picker before showing a fresh one.
13177
- host.querySelectorAll('.lp-project-picker').forEach((el) => el.remove());
13178
-
13179
- const picker = document.createElement('div');
13180
- picker.className = 'lp-project-picker';
13181
-
13182
- // ── Step 1: destination scope ──────────────────────────────────────────
13183
- const destLabel = document.createElement('div');
13184
- destLabel.className = 'lp-pp-label';
13185
- destLabel.textContent = 'Share to:';
13186
- picker.appendChild(destLabel);
13187
-
13188
- const destRow = document.createElement('div');
13189
- destRow.className = 'lp-pp-dest-row';
13190
-
13191
- const makeDestBtn = (value, text, desc) => {
13192
- const btn = document.createElement('button');
13193
- btn.type = 'button';
13194
- btn.className = 'lp-pp-dest-btn';
13195
- btn.dataset.dest = value;
13196
- const name = document.createElement('span'); name.className = 'lp-pp-dest-name'; name.textContent = text;
13197
- const sub = document.createElement('span'); sub.className = 'lp-pp-dest-sub'; sub.textContent = desc;
13198
- btn.appendChild(name); btn.appendChild(sub);
13199
- return btn;
13200
- };
13201
-
13202
- const companyBtn = makeDestBtn('org', 'Company', 'Everyone in the organization');
13203
- const projectBtn = makeDestBtn('project', 'This project', 'Visible on a specific project');
13204
- destRow.appendChild(companyBtn);
13205
- destRow.appendChild(projectBtn);
13206
- picker.appendChild(destRow);
13207
-
13208
- // ── Step 2: project picker (hidden until "This project" chosen) ────────
13209
- const projectSection = document.createElement('div');
13210
- projectSection.className = 'lp-pp-project-section';
13211
- projectSection.hidden = true;
13163
+ // Destination picker: modal overlay. Company vs. This project, then (for project)
13164
+ // optionally which project when more than one is loaded.
13165
+ function tfShareDestinationPicker(scope, level, selectedTitles, job) {
13166
+ const modal = document.getElementById('share-dest-modal');
13167
+ if (!modal) return;
13212
13168
 
13169
+ const orgBtn = document.getElementById('share-dest-org');
13170
+ const projBtn = document.getElementById('share-dest-proj');
13171
+ const projSection = document.getElementById('share-dest-proj-section');
13172
+ const projSel = document.getElementById('share-dest-proj-sel');
13173
+ const goBtn = document.getElementById('share-dest-go');
13174
+ const cancelBtn = document.getElementById('share-dest-cancel');
13175
+ const hintEl = document.getElementById('share-dest-hint');
13176
+
13177
+ if (!orgBtn || !projBtn || !projSection || !projSel || !goBtn || !cancelBtn) return;
13178
+
13179
+ // Reset visual state.
13180
+ orgBtn.classList.remove('lp-pp-dest-selected');
13181
+ projBtn.classList.remove('lp-pp-dest-selected');
13182
+ projSection.hidden = true;
13183
+ goBtn.disabled = true;
13184
+ if (hintEl) hintEl.textContent = selectedTitles && selectedTitles.length > 0
13185
+ ? `${selectedTitles.length} learning${selectedTitles.length === 1 ? '' : 's'} selected`
13186
+ : 'All learnings will be shared';
13187
+
13188
+ // Populate project dropdown.
13189
+ projSel.innerHTML = '';
13213
13190
  const projects = (typeof tf !== 'undefined' && tf.projects) || [];
13214
- if (projects.length > 1) {
13215
- const projLabel = document.createElement('label');
13216
- projLabel.className = 'lp-pp-label';
13217
- const projLabelId = 'lp-pp-proj-' + Math.random().toString(36).slice(2, 8);
13218
- projLabel.id = projLabelId;
13219
- projLabel.textContent = 'Which project?';
13220
- const projSel = document.createElement('select');
13221
- projSel.className = 'lp-pp-sel';
13222
- projSel.setAttribute('aria-labelledby', projLabelId);
13191
+ if (projects.length > 0) {
13223
13192
  for (const p of projects) {
13224
- const o = document.createElement('option'); o.value = p.id;
13193
+ const o = document.createElement('option');
13194
+ o.value = p.id;
13225
13195
  o.textContent = p.name || p.id;
13226
- if (p.id === tf.activeProjectId) o.selected = true;
13196
+ if (p.id === (typeof tf !== 'undefined' && tf.activeProjectId)) o.selected = true;
13227
13197
  projSel.appendChild(o);
13228
13198
  }
13229
- projectSection.appendChild(projLabel);
13230
- projectSection.appendChild(projSel);
13231
- projectSection._projSel = projSel;
13199
+ } else if (state.projectPath) {
13200
+ const o = document.createElement('option');
13201
+ o.value = state.projectPath;
13202
+ o.textContent = state.projectPath.split(/[\\/]/).pop() || state.projectPath;
13203
+ projSel.appendChild(o);
13232
13204
  }
13233
- picker.appendChild(projectSection);
13234
-
13235
- // ── Action row ─────────────────────────────────────────────────────────
13236
- const bar = document.createElement('div'); bar.className = 'lp-pp-bar';
13237
- const go = document.createElement('button');
13238
- go.type = 'button'; go.className = 'rb-approve'; go.textContent = 'Start'; go.disabled = true;
13239
- const cancel = document.createElement('button');
13240
- cancel.type = 'button'; cancel.className = 'rb-secondary'; cancel.textContent = 'Cancel';
13241
- bar.appendChild(go); bar.appendChild(cancel);
13242
- picker.appendChild(bar);
13243
- host.appendChild(picker);
13244
13205
 
13245
13206
  let chosenDest = null;
13246
13207
 
13247
- const selectDest = (btn) => {
13248
- destRow.querySelectorAll('.lp-pp-dest-btn').forEach((b) => b.classList.remove('lp-pp-dest-selected'));
13249
- btn.classList.add('lp-pp-dest-selected');
13250
- chosenDest = btn.dataset.dest;
13251
- projectSection.hidden = chosenDest !== 'project';
13252
- go.disabled = false;
13208
+ // Use AbortController so listeners are torn down atomically when the modal closes.
13209
+ const ac = new AbortController();
13210
+ const sig = { signal: ac.signal };
13211
+
13212
+ const selectDest = (dest) => {
13213
+ orgBtn.classList.toggle('lp-pp-dest-selected', dest === 'org');
13214
+ projBtn.classList.toggle('lp-pp-dest-selected', dest === 'project');
13215
+ chosenDest = dest;
13216
+ projSection.hidden = dest !== 'project';
13217
+ goBtn.disabled = false;
13218
+ };
13219
+
13220
+ orgBtn.addEventListener('click', () => selectDest('org'), sig);
13221
+ projBtn.addEventListener('click', () => selectDest('project'), sig);
13222
+
13223
+ const close = () => {
13224
+ ac.abort();
13225
+ modal.hidden = true;
13226
+ modal.classList.remove('open');
13253
13227
  };
13254
13228
 
13255
- companyBtn.addEventListener('click', () => selectDest(companyBtn));
13256
- projectBtn.addEventListener('click', () => selectDest(projectBtn));
13229
+ cancelBtn.addEventListener('click', close, sig);
13257
13230
 
13258
- cancel.addEventListener('click', () => picker.remove());
13231
+ goBtn.addEventListener('click', async () => {
13232
+ const dest = chosenDest || 'project';
13233
+ close();
13234
+ const projectId = dest === 'project'
13235
+ ? (projSel.value || ((typeof tf !== 'undefined' && tf.activeProjectId) || state.projectPath))
13236
+ : ((typeof tf !== 'undefined' && tf.activeProjectId) || state.projectPath);
13237
+ await tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, dest);
13238
+ }, sig);
13259
13239
 
13260
- go.addEventListener('click', async () => {
13261
- go.disabled = true;
13262
- picker.remove();
13263
- const projectId = chosenDest === 'project'
13264
- ? (projectSection._projSel ? projectSection._projSel.value : (tf.activeProjectId || state.projectPath))
13265
- : (tf.activeProjectId || state.projectPath);
13266
- await tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, chosenDest);
13267
- });
13240
+ // Dismiss on backdrop click.
13241
+ modal.addEventListener('click', (e) => { if (e.target === modal) close(); }, sig);
13242
+
13243
+ // Open.
13244
+ modal.hidden = false;
13245
+ modal.classList.add('open');
13246
+ orgBtn.focus();
13268
13247
  }
13269
13248
 
13270
13249
  async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, destination) {