aimeat 1.20.6 → 1.22.0
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/dist/.env.example +9 -0
- package/dist/locales/en.json +17 -1
- package/dist/locales/fi.json +17 -3
- package/dist/public/css/views/profile.css +55 -0
- package/dist/public/js/services/memory.js +9 -2
- package/dist/public/views/profile/landing-page.js +16 -1
- package/dist/public/views/profile/organisms-tab.js +456 -220
- package/dist/src/cli/connect/api-client.d.ts +24 -0
- package/dist/src/cli/connect/api-client.d.ts.map +1 -1
- package/dist/src/cli/connect/api-client.js +29 -33
- package/dist/src/cli/connect/api-client.js.map +1 -1
- package/dist/src/cli/connect/config.d.ts.map +1 -1
- package/dist/src/cli/connect/config.js +4 -1
- package/dist/src/cli/connect/config.js.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.d.ts +25 -0
- package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -0
- package/dist/src/cli/connect/mcp/local-server.js +411 -0
- package/dist/src/cli/connect/mcp/local-server.js.map +1 -0
- package/dist/src/cli/connect/mcp/poller.d.ts +13 -0
- package/dist/src/cli/connect/mcp/poller.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/poller.js +3 -2
- package/dist/src/cli/connect/mcp/poller.js.map +1 -1
- package/dist/src/cli/connect/mcp/server.d.ts +41 -0
- package/dist/src/cli/connect/mcp/server.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/server.js +47 -21
- package/dist/src/cli/connect/mcp/server.js.map +1 -1
- package/dist/src/cli/connect/tunnel-client.d.ts +109 -0
- package/dist/src/cli/connect/tunnel-client.d.ts.map +1 -0
- package/dist/src/cli/connect/tunnel-client.js +462 -0
- package/dist/src/cli/connect/tunnel-client.js.map +1 -0
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +9 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +139 -0
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/index.js +72 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/models/schemas.d.ts +1 -1
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +6 -1
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/connect-tunnel.d.ts +17 -0
- package/dist/src/routes/connect-tunnel.d.ts.map +1 -0
- package/dist/src/routes/connect-tunnel.js +31 -0
- package/dist/src/routes/connect-tunnel.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +5 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts +2 -0
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +11 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +1 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +5 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/connect-tunnel.d.ts +123 -0
- package/dist/src/services/connect-tunnel.d.ts.map +1 -0
- package/dist/src/services/connect-tunnel.js +403 -0
- package/dist/src/services/connect-tunnel.js.map +1 -0
- package/dist/src/services/event-bus.d.ts +14 -0
- package/dist/src/services/event-bus.d.ts.map +1 -1
- package/dist/src/services/event-bus.js +25 -0
- package/dist/src/services/event-bus.js.map +1 -1
- package/package.json +1 -1
|
@@ -60,12 +60,21 @@
|
|
|
60
60
|
* name with a role (viewer/contributor), switch roles, remove; approve/deny pending requests. New
|
|
61
61
|
* "Create contract agent" AI-prompt button (orgService.buildContractAgentPrompt) copies a full
|
|
62
62
|
* contract-agent build prompt for THIS workspace. See docs/agent-workspace-contracts.md.
|
|
63
|
+
* v1.15.0 — 2026-06-10 — Organisms list renew, phase 1: compact one-line rows (avatar, name, type,
|
|
64
|
+
* members/agents/date) with drag-and-drop manual ordering + a sort menu (My order / Name / Newest,
|
|
65
|
+
* persisted to user memory key organisms.ui); management actions move into a "…" kebab menu. New
|
|
66
|
+
* OrganismHome page (breadcrumb header + Export/Settings + Workspaces/Members/Agents/Board tabs)
|
|
67
|
+
* between the list and a workspace — Members/Agents tabs host the former in-card OrgMemberManager
|
|
68
|
+
* sections, Settings hosts the edit form + metadata + danger zone, Board links to the Boards tab.
|
|
69
|
+
* Nothing removed: every former card action has a new home (list "…" menu, home header, or a tab).
|
|
70
|
+
* v1.16.0 — 2026-06-10 — Row button "Open workspace" → "Open"; new 📁 workspace-count stat next to
|
|
71
|
+
* the members counter (lazy discoverWorkspaces per own org) so empty organisms are obvious.
|
|
63
72
|
*/
|
|
64
73
|
import { h } from 'preact';
|
|
65
|
-
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
74
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'preact/hooks';
|
|
66
75
|
import htm from 'htm';
|
|
67
76
|
const html = htm.bind(h);
|
|
68
|
-
import { t } from '/js/i18n.js';
|
|
77
|
+
import { t, getLocale } from '/js/i18n.js';
|
|
69
78
|
import { escHtml } from '/js/utils.js';
|
|
70
79
|
import { Spinner, VisibilityPill } from './shared.js';
|
|
71
80
|
import { useConfirm } from '/components/Modal.js';
|
|
@@ -81,6 +90,58 @@ import { dt, fmtBytes } from '/js/format.js';
|
|
|
81
90
|
import { Markdown, slugifyHeading } from '/components/Markdown.js';
|
|
82
91
|
import { Mermaid } from '/components/Mermaid.js';
|
|
83
92
|
|
|
93
|
+
/** Date-only, formatted in the APP locale (browser locale may differ — fi must show 10.6.2026, not 6/10/2026). */
|
|
94
|
+
function fmtDate(s) {
|
|
95
|
+
return new Date(s).toLocaleDateString(getLocale() === 'fi' ? 'fi-FI' : undefined);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Two-letter monogram for the list/home avatar (initials of the first two words, else first two chars). */
|
|
99
|
+
function orgInitials(name) {
|
|
100
|
+
const s = String(name || '?').trim();
|
|
101
|
+
const words = s.split(/\s+/).filter(Boolean);
|
|
102
|
+
const ini = words.length >= 2 ? words[0][0] + words[1][0] : s.slice(0, 2);
|
|
103
|
+
return ini.charAt(0).toUpperCase() + (ini.charAt(1) || '').toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** "…" actions menu — items: { label, icon?, danger?, onClick } (falsy items are skipped). Closes on outside click. */
|
|
107
|
+
function KebabMenu({ items, label }) {
|
|
108
|
+
const [open, setOpen] = useState(false);
|
|
109
|
+
const ref = useRef(null);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!open) return undefined;
|
|
112
|
+
const close = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
|
113
|
+
document.addEventListener('click', close);
|
|
114
|
+
return () => document.removeEventListener('click', close);
|
|
115
|
+
}, [open]);
|
|
116
|
+
const visible = (items || []).filter(Boolean);
|
|
117
|
+
if (visible.length === 0) return null;
|
|
118
|
+
return html`
|
|
119
|
+
<div class="pj-menu" ref=${ref}>
|
|
120
|
+
<button class="pj-icon-btn pj-menu-btn" title=${label} aria-haspopup="menu" aria-expanded=${open}
|
|
121
|
+
onClick=${(e) => { e.stopPropagation(); setOpen(o => !o); }}>${'⋮'}</button>
|
|
122
|
+
${open ? html`
|
|
123
|
+
<div class="pj-menu-pop" role="menu" onClick=${(e) => e.stopPropagation()}>
|
|
124
|
+
${visible.map(it => html`
|
|
125
|
+
<button class="pj-menu-item ${it.danger ? 'pj-menu-item-danger' : ''}" role="menuitem" key=${it.label}
|
|
126
|
+
onClick=${() => { setOpen(false); it.onClick?.(); }}>${it.icon ? `${it.icon} ` : ''}${it.label}</button>`)}
|
|
127
|
+
</div>` : null}
|
|
128
|
+
</div>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Download a whole-organism ZIP backup (used from the list "…" menu and the home header). */
|
|
132
|
+
async function exportOrganismZip(org, showToast) {
|
|
133
|
+
try {
|
|
134
|
+
const jwt = window.AIMEAT?.auth?.getSession?.()?.jwt || '';
|
|
135
|
+
const res = await fetch(`/v1/organisms/${encodeURIComponent(org.id)}/export`, { headers: { Authorization: 'Bearer ' + jwt } });
|
|
136
|
+
if (!res.ok) throw new Error('Export failed');
|
|
137
|
+
const blob = await res.blob();
|
|
138
|
+
const url = URL.createObjectURL(blob);
|
|
139
|
+
const a = document.createElement('a');
|
|
140
|
+
a.href = url; a.download = `organism-${String(org.name || org.id).replace(/[^a-z0-9_-]+/gi, '-').slice(0, 40)}.zip`;
|
|
141
|
+
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
|
|
142
|
+
} catch (e) { showToast((e && e.message) || 'Export failed'); }
|
|
143
|
+
}
|
|
144
|
+
|
|
84
145
|
/**
|
|
85
146
|
* Content search inside an organism — case-insensitive substring over the records + documents of
|
|
86
147
|
* every workspace the member can read. Backend: GET /v1/organisms/:id/search. Rendered in the
|
|
@@ -177,7 +238,11 @@ function IncomingInvitations({ showToast, onChanged }) {
|
|
|
177
238
|
* list (#6). Backend: /:id/{join-requests,invitations,members,transfer,agents}. Refreshes the
|
|
178
239
|
* parent list via onChanged so member counts stay current.
|
|
179
240
|
*/
|
|
180
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Member + agent management. `show` limits the rendered sections: 'members' (invite, pending
|
|
243
|
+
* requests, roster, blocked) or 'agents' (attached agents); omitted = both (legacy layout).
|
|
244
|
+
*/
|
|
245
|
+
function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, confirm, onChanged, show }) {
|
|
181
246
|
const orgId = org.id;
|
|
182
247
|
const [requests, setRequests] = useState(null);
|
|
183
248
|
const [members, setMembers] = useState(null);
|
|
@@ -259,8 +324,12 @@ function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, confirm,
|
|
|
259
324
|
const pending = (requests || []).filter(r => r.status === 'pending');
|
|
260
325
|
const agents = org.agentGaiis || [];
|
|
261
326
|
|
|
327
|
+
const showMembers = show !== 'agents';
|
|
328
|
+
const showAgents = show !== 'members';
|
|
329
|
+
|
|
262
330
|
return html`
|
|
263
331
|
<div class="card-detail">
|
|
332
|
+
${showMembers ? html`
|
|
264
333
|
<div class="section-title">${canManage ? (t('organisms.manageMembers') || 'Manage members') : (t('organisms.membersRoster') || 'Members')}</div>
|
|
265
334
|
${canManage ? html`<div class="section-desc">${t('organisms.manageMembersDesc') || 'Invite, approve, block and revoke member access.'}</div>` : null}
|
|
266
335
|
|
|
@@ -314,7 +383,9 @@ function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, confirm,
|
|
|
314
383
|
</div>
|
|
315
384
|
`)}
|
|
316
385
|
` : null}
|
|
386
|
+
` : null}
|
|
317
387
|
|
|
388
|
+
${showAgents ? html`
|
|
318
389
|
<div class="detail-label">${t('organisms.attachedAgents') || 'Attached agents'}</div>
|
|
319
390
|
${agents.length === 0 ? html`<div class="section-desc">${t('organisms.noAgents') || 'No agents attached.'}</div>` : null}
|
|
320
391
|
${agents.map(g => html`
|
|
@@ -330,6 +401,7 @@ function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, confirm,
|
|
|
330
401
|
onInput=${(e) => setAgentName(e.target.value)} onKeyDown=${(e) => { if (e.key === 'Enter') attachAgent(); }} />
|
|
331
402
|
<button class="btn-outline btn-sm" disabled=${busy || !agentName.trim()} onClick=${attachAgent}>${t('organisms.attachAgent') || 'Attach agent'}</button>
|
|
332
403
|
</div>
|
|
404
|
+
` : null}
|
|
333
405
|
</div>
|
|
334
406
|
`;
|
|
335
407
|
}
|
|
@@ -345,9 +417,13 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
345
417
|
const [openWs, setOpenWs] = useState(() => { try { return sessionStorage.getItem('aimeat.ws.openWs') || null; } catch (e) { return null; } });
|
|
346
418
|
const [creating, setCreating] = useState(false);
|
|
347
419
|
const [showCreate, setShowCreate] = useState(false);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const [
|
|
420
|
+
// List ordering: 'custom' (drag-and-drop, persisted) | 'name' | 'newest'. Persisted together with
|
|
421
|
+
// the manual order in the user-memory key `organisms.ui` so it follows the user across devices.
|
|
422
|
+
const [sortMode, setSortMode] = useState('custom');
|
|
423
|
+
const [customOrder, setCustomOrder] = useState([]);
|
|
424
|
+
const [openSettings, setOpenSettings] = useState(false); // open OrganismHome with the Settings panel showing
|
|
425
|
+
const dragIdRef = useRef(null);
|
|
426
|
+
const [dragOverId, setDragOverId] = useState(null);
|
|
351
427
|
|
|
352
428
|
/* ── Create form state ── */
|
|
353
429
|
const [formName, setFormName] = useState('');
|
|
@@ -358,6 +434,7 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
358
434
|
const [formInterests, setFormInterests] = useState('');
|
|
359
435
|
|
|
360
436
|
const ghii = session?.owner || '';
|
|
437
|
+
const [wsCounts, setWsCounts] = useState({}); // orgId → workspace count (own orgs)
|
|
361
438
|
|
|
362
439
|
const loadData = useCallback(async () => {
|
|
363
440
|
try {
|
|
@@ -371,6 +448,10 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
371
448
|
setMyOrganisms(mine);
|
|
372
449
|
setPublicOrganisms(discover);
|
|
373
450
|
onStats?.({ organisms: mine.length });
|
|
451
|
+
// Workspace counts for the 📁 row stat (member-gated endpoint, so own orgs only).
|
|
452
|
+
Promise.all(mine.map(async (o) => [o.id, (await orgService.discoverWorkspaces(o.id)).length]))
|
|
453
|
+
.then(entries => setWsCounts(Object.fromEntries(entries)))
|
|
454
|
+
.catch(() => {});
|
|
374
455
|
} catch {
|
|
375
456
|
setMyOrganisms([]);
|
|
376
457
|
setPublicOrganisms([]);
|
|
@@ -381,6 +462,53 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
381
462
|
if (session) loadData();
|
|
382
463
|
}, [session, loadData]);
|
|
383
464
|
|
|
465
|
+
// ── List-order preferences (user memory key `organisms.ui`: { order: [ids], sort: mode }) ──
|
|
466
|
+
const UI_PREFS_KEY = 'organisms.ui';
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
if (!session) return;
|
|
469
|
+
(async () => {
|
|
470
|
+
try {
|
|
471
|
+
const r = await memoryService.getMemory(UI_PREFS_KEY);
|
|
472
|
+
const v = r?.data?.value;
|
|
473
|
+
if (v && typeof v === 'object') {
|
|
474
|
+
if (Array.isArray(v.order)) setCustomOrder(v.order.filter(x => typeof x === 'string'));
|
|
475
|
+
if (['custom', 'name', 'newest'].includes(v.sort)) setSortMode(v.sort);
|
|
476
|
+
}
|
|
477
|
+
} catch { /* no saved prefs yet */ }
|
|
478
|
+
})();
|
|
479
|
+
}, [session]);
|
|
480
|
+
const savePrefs = useCallback((order, sort) => {
|
|
481
|
+
memoryService.createMemory(UI_PREFS_KEY, { order, sort }, 'private').catch(() => {});
|
|
482
|
+
}, []);
|
|
483
|
+
|
|
484
|
+
// Stable sort keeps server order for ids missing from the saved manual order (appended at the end).
|
|
485
|
+
const sortedMine = useMemo(() => {
|
|
486
|
+
const arr = [...(myOrganisms || [])];
|
|
487
|
+
if (sortMode === 'name') arr.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
|
|
488
|
+
else if (sortMode === 'newest') arr.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
|
|
489
|
+
else if (customOrder.length) {
|
|
490
|
+
const pos = new Map(customOrder.map((id, i) => [id, i]));
|
|
491
|
+
arr.sort((a, b) => (pos.has(a.id) ? pos.get(a.id) : Infinity) - (pos.has(b.id) ? pos.get(b.id) : Infinity));
|
|
492
|
+
}
|
|
493
|
+
return arr;
|
|
494
|
+
}, [myOrganisms, sortMode, customOrder]);
|
|
495
|
+
|
|
496
|
+
// Drop on a row: move the dragged row to the target's position; switches to manual order and saves.
|
|
497
|
+
const onDropRow = (targetId) => {
|
|
498
|
+
const fromId = dragIdRef.current;
|
|
499
|
+
dragIdRef.current = null;
|
|
500
|
+
setDragOverId(null);
|
|
501
|
+
if (!fromId || fromId === targetId) return;
|
|
502
|
+
const ids = sortedMine.map(o => o.id);
|
|
503
|
+
const from = ids.indexOf(fromId);
|
|
504
|
+
const to = ids.indexOf(targetId);
|
|
505
|
+
if (from < 0 || to < 0) return;
|
|
506
|
+
ids.splice(to, 0, ids.splice(from, 1)[0]);
|
|
507
|
+
setCustomOrder(ids);
|
|
508
|
+
setSortMode('custom');
|
|
509
|
+
savePrefs(ids, 'custom');
|
|
510
|
+
};
|
|
511
|
+
|
|
384
512
|
// Import a whole organism from a ZIP backup → creates a new organism + opens it.
|
|
385
513
|
const orgFileRef = useRef(null);
|
|
386
514
|
const [importingOrg, setImportingOrg] = useState(false);
|
|
@@ -469,6 +597,7 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
469
597
|
try {
|
|
470
598
|
await orgService.leaveOrganism(id);
|
|
471
599
|
showToast(t('organisms.left') || 'Left organism');
|
|
600
|
+
setOpenId(null); setOpenWs(null);
|
|
472
601
|
loadData();
|
|
473
602
|
} catch {
|
|
474
603
|
showToast(t('organisms.leaveError') || 'Failed to leave');
|
|
@@ -482,6 +611,7 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
482
611
|
await orgService.deleteOrganism(id);
|
|
483
612
|
showToast(t('organisms.deleted') || 'Organism deleted');
|
|
484
613
|
setExpanded(null);
|
|
614
|
+
setOpenId(null); setOpenWs(null);
|
|
485
615
|
loadData();
|
|
486
616
|
} catch {
|
|
487
617
|
showToast(t('organisms.deleteError') || 'Failed to delete');
|
|
@@ -493,212 +623,113 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
493
623
|
setExpanded(prev => prev === id ? null : id);
|
|
494
624
|
}, []);
|
|
495
625
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
name: org.name,
|
|
500
|
-
description: org.description || '',
|
|
501
|
-
type: org.type,
|
|
502
|
-
join_policy: org.joinPolicy,
|
|
503
|
-
visibility: org.visibility,
|
|
504
|
-
interests: (org.interests || []).join(', '),
|
|
505
|
-
});
|
|
506
|
-
}, []);
|
|
507
|
-
|
|
508
|
-
const cancelEdit = useCallback(() => {
|
|
509
|
-
setEditing(null);
|
|
510
|
-
setEditForm({});
|
|
511
|
-
}, []);
|
|
512
|
-
|
|
513
|
-
const handleUpdate = useCallback(async (id) => {
|
|
514
|
-
if (!editForm.name?.trim()) {
|
|
515
|
-
showToast(t('organisms.nameRequired') || 'Name is required');
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
setSaving(true);
|
|
519
|
-
try {
|
|
520
|
-
const interests = editForm.interests.split(',').map(s => s.trim()).filter(Boolean);
|
|
521
|
-
const result = await orgService.updateOrganism(id, {
|
|
522
|
-
name: editForm.name.trim(),
|
|
523
|
-
description: editForm.description.trim(),
|
|
524
|
-
type: editForm.type,
|
|
525
|
-
join_policy: editForm.join_policy,
|
|
526
|
-
visibility: editForm.visibility,
|
|
527
|
-
interests,
|
|
528
|
-
});
|
|
529
|
-
if (result?.ok !== false) {
|
|
530
|
-
showToast(t('organisms.updated') || 'Organism updated');
|
|
531
|
-
setEditing(null);
|
|
532
|
-
setEditForm({});
|
|
533
|
-
loadData();
|
|
534
|
-
} else {
|
|
535
|
-
showToast(result?.error?.message || (t('organisms.updateError') || 'Failed to update'));
|
|
536
|
-
}
|
|
537
|
-
} catch {
|
|
538
|
-
showToast(t('organisms.updateError') || 'Failed to update');
|
|
539
|
-
} finally { setSaving(false); }
|
|
540
|
-
}, [editForm, showToast, loadData]);
|
|
541
|
-
|
|
542
|
-
const renderEditForm = (org) => html`
|
|
543
|
-
<div class="card-detail" onClick=${(e) => e.stopPropagation()}>
|
|
544
|
-
<div class="flex-col">
|
|
545
|
-
<input type="text" value=${editForm.name} onInput=${(e) => setEditForm(f => ({ ...f, name: e.target.value }))}
|
|
546
|
-
placeholder=${t('organisms.namePlaceholder') || 'Name'} class="input-field input-sm" />
|
|
547
|
-
<textarea value=${editForm.description} onInput=${(e) => setEditForm(f => ({ ...f, description: e.target.value }))} rows="2"
|
|
548
|
-
placeholder=${t('organisms.descPlaceholder') || 'Description'} class="input-field input-sm" />
|
|
549
|
-
<input type="text" value=${editForm.interests} onInput=${(e) => setEditForm(f => ({ ...f, interests: e.target.value }))}
|
|
550
|
-
placeholder=${t('organisms.interestsPlaceholder') || 'Interests (comma separated)'} class="input-field input-sm" />
|
|
551
|
-
<div class="flex-row-wrap">
|
|
552
|
-
<select value=${editForm.type} onChange=${(e) => setEditForm(f => ({ ...f, type: e.target.value }))} class="input-field input-sm">
|
|
553
|
-
<option value="community">${t('organisms.types.community') || 'Community'}</option>
|
|
554
|
-
<option value="team">${t('organisms.types.team') || 'Team'}</option>
|
|
555
|
-
<option value="club">${t('organisms.types.club') || 'Club'}</option>
|
|
556
|
-
<option value="cooperative">${t('organisms.types.cooperative') || 'Cooperative'}</option>
|
|
557
|
-
<option value="project">${t('organisms.types.project') || 'Project'}</option>
|
|
558
|
-
</select>
|
|
559
|
-
<select value=${editForm.join_policy} onChange=${(e) => setEditForm(f => ({ ...f, join_policy: e.target.value }))} class="input-field input-sm">
|
|
560
|
-
<option value="open">${t('organisms.policyOpen') || 'Open (anyone can join)'}</option>
|
|
561
|
-
<option value="approval_required">${t('organisms.policyApproval') || 'Approval required'}</option>
|
|
562
|
-
<option value="invite_only">${t('organisms.policyInvite') || 'Invite only'}</option>
|
|
563
|
-
</select>
|
|
564
|
-
<select value=${editForm.visibility} onChange=${(e) => setEditForm(f => ({ ...f, visibility: e.target.value }))} class="input-field input-sm">
|
|
565
|
-
<option value="public">${t('organisms.visPublic') || 'Public'}</option>
|
|
566
|
-
<option value="listed">${t('organisms.visListed') || 'Listed'}</option>
|
|
567
|
-
<option value="private">${t('organisms.visPrivate') || 'Private'}</option>
|
|
568
|
-
</select>
|
|
569
|
-
</div>
|
|
570
|
-
<div class="flex-row-wrap">
|
|
571
|
-
<button class="btn-primary btn-sm" onClick=${() => handleUpdate(org.id)} disabled=${saving}>
|
|
572
|
-
${saving ? '...' : (t('organisms.save') || 'Save')}
|
|
573
|
-
</button>
|
|
574
|
-
<button class="btn-ghost btn-sm" onClick=${cancelEdit}>
|
|
575
|
-
${t('organisms.cancel') || 'Cancel'}
|
|
576
|
-
</button>
|
|
577
|
-
</div>
|
|
578
|
-
</div>
|
|
579
|
-
</div>
|
|
580
|
-
`;
|
|
581
|
-
|
|
582
|
-
const renderOrgCard = (org, isMine) => {
|
|
583
|
-
const isExpanded = expanded === org.id;
|
|
584
|
-
const isEditing = editing === org.id;
|
|
585
|
-
const isCreator = org.creatorGhii === ghii;
|
|
586
|
-
const isAdmin = org.admins?.includes(ghii);
|
|
587
|
-
const isMember = org.members?.includes(ghii);
|
|
588
|
-
const canEdit = isCreator || isAdmin;
|
|
626
|
+
// Discover rows expand in place (non-members can't open the home page, so the detail
|
|
627
|
+
// grid + interests they could previously see in the expanded card stay reachable here).
|
|
628
|
+
const renderDiscoverDetail = (org) => {
|
|
589
629
|
const policyLabel = {
|
|
590
630
|
open: t('organisms.policyOpen') || 'Open',
|
|
591
631
|
approval_required: t('organisms.policyApproval') || 'Approval',
|
|
592
632
|
invite_only: t('organisms.policyInvite') || 'Invite Only',
|
|
593
633
|
}[org.joinPolicy] || org.joinPolicy;
|
|
594
|
-
const typeLabel = t(`organisms.types.${org.type}`) || org.type;
|
|
595
|
-
|
|
596
634
|
return html`
|
|
597
|
-
<div class="
|
|
598
|
-
<div class="
|
|
599
|
-
<
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
<span class="badge ${org.visibility === 'public' ? 'badge-success' : 'badge-warn'}">${org.visibility}</span>
|
|
603
|
-
</div>
|
|
604
|
-
<div class="card-subtitle">
|
|
605
|
-
${org.description ? escHtml(org.description.slice(0, 100)) : ''}
|
|
606
|
-
${' \u2014 '}${(org.members || []).length} ${t('organisms.members') || 'members'}
|
|
607
|
-
${' \u2014 '}${policyLabel}
|
|
608
|
-
</div>
|
|
609
|
-
|
|
610
|
-
${(org.interests || []).length > 0 && html`
|
|
611
|
-
<div class="flex-row-wrap" >
|
|
612
|
-
${org.interests.map(tag => html`<span class="file-tag" key=${tag}>${escHtml(tag)}</span>`)}
|
|
635
|
+
<div class="pj-org-detail" onClick=${(e) => e.stopPropagation()}>
|
|
636
|
+
<div class="detail-grid">
|
|
637
|
+
<div class="detail-item">
|
|
638
|
+
<span class="detail-label">${t('organisms.creator') || 'Creator'}</span>
|
|
639
|
+
<span class="detail-value">${escHtml(org.creatorGhii)}</span>
|
|
613
640
|
</div>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
</div>
|
|
627
|
-
<div class="detail-item">
|
|
628
|
-
<span class="detail-label">${t('organisms.memberCount') || 'Members'}</span>
|
|
629
|
-
<span class="detail-value">${(org.members || []).length} / ${org.maxMembers || 500}</span>
|
|
630
|
-
</div>
|
|
631
|
-
${org.boardId ? html`
|
|
632
|
-
<div class="detail-item">
|
|
633
|
-
<span class="detail-label">${t('organisms.board') || 'Board'}</span>
|
|
634
|
-
<span class="detail-value mono">${escHtml(org.boardId)}</span>
|
|
635
|
-
</div>
|
|
636
|
-
` : null}
|
|
637
|
-
${org.createdAt ? html`
|
|
638
|
-
<div class="detail-item">
|
|
639
|
-
<span class="detail-label">${t('organisms.createdAt') || 'Created'}</span>
|
|
640
|
-
<span class="detail-value">${new Date(org.createdAt).toLocaleDateString()}</span>
|
|
641
|
-
</div>
|
|
642
|
-
` : null}
|
|
641
|
+
<div class="detail-item">
|
|
642
|
+
<span class="detail-label">${t('organisms.memberCount') || 'Members'}</span>
|
|
643
|
+
<span class="detail-value">${(org.members || []).length} / ${org.maxMembers || 500}</span>
|
|
644
|
+
</div>
|
|
645
|
+
<div class="detail-item">
|
|
646
|
+
<span class="detail-label">${t('organisms.policyLabel') || 'Join policy'}</span>
|
|
647
|
+
<span class="detail-value">${policyLabel}</span>
|
|
648
|
+
</div>
|
|
649
|
+
${org.createdAt ? html`
|
|
650
|
+
<div class="detail-item">
|
|
651
|
+
<span class="detail-label">${t('organisms.createdAt') || 'Created'}</span>
|
|
652
|
+
<span class="detail-value">${fmtDate(org.createdAt)}</span>
|
|
643
653
|
</div>
|
|
654
|
+
` : null}
|
|
655
|
+
</div>
|
|
656
|
+
${(org.interests || []).length > 0 ? html`
|
|
657
|
+
<div class="flex-row-wrap">
|
|
658
|
+
${org.interests.map(tag => html`<span class="file-tag" key=${tag}>${escHtml(tag)}</span>`)}
|
|
659
|
+
</div>` : null}
|
|
660
|
+
</div>`;
|
|
661
|
+
};
|
|
644
662
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
</button>
|
|
665
|
-
` : null}
|
|
666
|
-
${!isMember ? html`
|
|
667
|
-
<button class="btn-primary btn-sm" onClick=${(e) => { e.stopPropagation(); handleJoin(org.id); }}>
|
|
668
|
-
${t('organisms.join') || 'Join'}
|
|
669
|
-
</button>
|
|
670
|
-
` : null}
|
|
671
|
-
</div>
|
|
663
|
+
// Compact one-line row: avatar \u00B7 name + type + lock \u00B7 description \u00B7 members/agents/date \u00B7
|
|
664
|
+
// primary action \u00B7 "\u2026" menu. Management (settings, leave, delete) lives in the menu / home page.
|
|
665
|
+
const renderOrgRow = (org, isMine) => {
|
|
666
|
+
const isCreator = org.creatorGhii === ghii;
|
|
667
|
+
const isAdmin = org.admins?.includes(ghii);
|
|
668
|
+
const isMember = org.members?.includes(ghii);
|
|
669
|
+
const canEdit = isCreator || isAdmin;
|
|
670
|
+
const typeLabel = t(`organisms.types.${org.type}`) || org.type;
|
|
671
|
+
const isExpanded = !isMine && expanded === org.id;
|
|
672
|
+
const visLabel = org.visibility === 'private' ? (t('organisms.visPrivate') || 'Private') : (t('organisms.visListed') || 'Listed');
|
|
673
|
+
const openHome = (withSettings) => { setOpenSettings(!!withSettings); setOpenWs(null); setOpenId(org.id); };
|
|
674
|
+
const activate = () => (isMine || isMember) ? openHome(false) : toggleExpand(org.id);
|
|
675
|
+
|
|
676
|
+
const menuItems = isMine ? [
|
|
677
|
+
canEdit && { label: t('organisms.settings') || 'Settings', icon: '\u2699', onClick: () => openHome(true) },
|
|
678
|
+
{ label: t('organisms.exportOrg') || 'Export organism', icon: '\u2B07', onClick: () => exportOrganismZip(org, showToast) },
|
|
679
|
+
!isCreator && { label: t('organisms.leave') || 'Leave', danger: true, onClick: () => handleLeave(org.id, org.name) },
|
|
680
|
+
isCreator && { label: t('organisms.delete') || 'Delete', danger: true, onClick: () => handleDelete(org.id, org.name) },
|
|
681
|
+
] : [];
|
|
672
682
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
683
|
+
return html`
|
|
684
|
+
<div class="pj-org-row ${dragOverId === org.id ? 'pj-org-drag-over' : ''}" key=${org.id}
|
|
685
|
+
draggable=${isMine}
|
|
686
|
+
onDragStart=${isMine ? ((e) => { dragIdRef.current = org.id; e.dataTransfer.effectAllowed = 'move'; }) : undefined}
|
|
687
|
+
onDragOver=${isMine ? ((e) => { e.preventDefault(); setDragOverId(org.id); }) : undefined}
|
|
688
|
+
onDragLeave=${isMine ? (() => setDragOverId(d => (d === org.id ? null : d))) : undefined}
|
|
689
|
+
onDrop=${isMine ? (() => onDropRow(org.id)) : undefined}
|
|
690
|
+
onDragEnd=${isMine ? (() => { dragIdRef.current = null; setDragOverId(null); }) : undefined}>
|
|
691
|
+
<div class="pj-org-avatar" aria-hidden="true">${orgInitials(org.name)}</div>
|
|
692
|
+
<div class="pj-org-main" role="button" tabindex="0" onClick=${activate}
|
|
693
|
+
onKeyDown=${(e) => { if (e.key === 'Enter') activate(); }}>
|
|
694
|
+
<div class="pj-org-titlerow">
|
|
695
|
+
<span class="pj-org-name">${escHtml(org.name)}</span>
|
|
696
|
+
<span class="badge badge-info">${typeLabel}</span>
|
|
697
|
+
${org.visibility !== 'public' ? html`<span class="pj-org-lock" title=${visLabel}>${'\uD83D\uDD12'}</span>` : null}
|
|
685
698
|
</div>
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
699
|
+
${org.description ? html`<div class="pj-org-desc">${escHtml(org.description)}</div>` : null}
|
|
700
|
+
</div>
|
|
701
|
+
<div class="pj-org-stats">
|
|
702
|
+
${isMine && wsCounts[org.id] !== undefined ? html`
|
|
703
|
+
<span class="pj-org-stat" title=${t('organisms.tabWorkspaces') || 'Workspaces'}>${'\uD83D\uDCC1'} ${wsCounts[org.id]}</span>` : null}
|
|
704
|
+
<span class="pj-org-stat" title=${t('organisms.members') || 'Members'}>${'\uD83D\uDC65'} ${(org.members || []).length}</span>
|
|
705
|
+
<span class="pj-org-stat" title=${t('organisms.attachedAgents') || 'Attached agents'}>${'\uD83E\uDD16'} ${(org.agentGaiis || []).length}</span>
|
|
706
|
+
${org.createdAt ? html`<span class="pj-org-stat pj-org-date" title=${t('organisms.createdAt') || 'Created'}>${fmtDate(org.createdAt)}</span>` : null}
|
|
707
|
+
</div>
|
|
708
|
+
${(isMine || isMember)
|
|
709
|
+
? html`<button class="btn-outline btn-sm pj-org-openbtn" onClick=${() => openHome(false)}>${t('organisms.open') || 'Open'}</button>`
|
|
710
|
+
: html`<button class="btn-outline btn-sm pj-org-openbtn" onClick=${() => handleJoin(org.id)}>${t('organisms.join') || 'Join'}</button>`}
|
|
711
|
+
${isMine ? html`<${KebabMenu} label=${t('organisms.moreActions') || 'More actions'} items=${menuItems} />` : null}
|
|
712
|
+
${isExpanded ? renderDiscoverDetail(org) : null}
|
|
689
713
|
</div>
|
|
690
714
|
`;
|
|
691
715
|
};
|
|
692
716
|
|
|
693
717
|
if (openId) {
|
|
694
718
|
const org = [...(myOrganisms || []), ...publicOrganisms].find(o => o.id === openId) || { id: openId };
|
|
695
|
-
// openWs chosen → that workspace; otherwise the organism's
|
|
719
|
+
// openWs chosen → that workspace; otherwise the organism's HOME page (tabs incl. workspaces).
|
|
696
720
|
if (openWs) {
|
|
697
721
|
return html`<${Workspace} org=${org} wsId=${openWs} session=${session} showToast=${showToast}
|
|
698
722
|
onBack=${() => { setOpenWs(null); }} />`;
|
|
699
723
|
}
|
|
700
|
-
return html
|
|
701
|
-
|
|
724
|
+
return html`
|
|
725
|
+
<${OrganismHome} org=${org} ghii=${ghii} showToast=${showToast}
|
|
726
|
+
initialSettings=${openSettings}
|
|
727
|
+
onOpenWs=${(wsId) => setOpenWs(wsId)}
|
|
728
|
+
onBack=${() => { setOpenId(null); setOpenSettings(false); loadData(); }}
|
|
729
|
+
onChanged=${loadData}
|
|
730
|
+
onLeave=${() => handleLeave(org.id, org.name)}
|
|
731
|
+
onDelete=${() => handleDelete(org.id, org.name)} />
|
|
732
|
+
<${ConfirmUI} />`;
|
|
702
733
|
}
|
|
703
734
|
|
|
704
735
|
if (!myOrganisms) return html`<${Spinner} text=${t('organisms.loading') || 'Loading organisms...'} />`;
|
|
@@ -707,13 +738,9 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
707
738
|
<div class="section-title">${t('organisms.title') || 'Organisms'}</div>
|
|
708
739
|
<div class="section-desc">${t('organisms.desc') || 'Organisms are groups — communities, teams, clubs, or projects. Create one or join existing ones to share knowledge, coordinate work, and build together.'}</div>
|
|
709
740
|
|
|
710
|
-
<!-- Create
|
|
741
|
+
<!-- Create form (opened from the topbar button) -->
|
|
711
742
|
<div class="mb-1">
|
|
712
|
-
${!showCreate ? html`
|
|
713
|
-
<button class="btn-primary" onClick=${() => setShowCreate(true)}>
|
|
714
|
-
${t('organisms.createNew') || 'Create Organism'}
|
|
715
|
-
</button>
|
|
716
|
-
` : html`
|
|
743
|
+
${!showCreate ? null : html`
|
|
717
744
|
<div class="create-form">
|
|
718
745
|
<h4 class="card-h3 mb-half">${t('organisms.createTitle') || 'Create New Organism'}</h4>
|
|
719
746
|
<div class="flex-col">
|
|
@@ -765,22 +792,242 @@ export default function OrganismsTab({ session, showToast, onStats }) {
|
|
|
765
792
|
<input type="file" accept=".zip,application/zip" ref=${orgFileRef} style="display:none" onChange=${(e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; doImportOrg(f); }} />
|
|
766
793
|
<div class="pj-ws-topbar">
|
|
767
794
|
<div class="section-title">${t('organisms.myOrganisms') || 'My Organisms'}</div>
|
|
768
|
-
<
|
|
795
|
+
<div class="pj-org-topbar-actions">
|
|
796
|
+
${myOrganisms.length > 1 ? html`
|
|
797
|
+
<select class="input-field input-sm pj-org-sort" title=${t('organisms.sortTitle') || 'Sort'} value=${sortMode}
|
|
798
|
+
onChange=${(e) => { const m = e.target.value; setSortMode(m); savePrefs(customOrder, m); }}>
|
|
799
|
+
<option value="custom">${t('organisms.sortCustom') || 'My order'}</option>
|
|
800
|
+
<option value="name">${t('organisms.sortName') || 'Name A–Z'}</option>
|
|
801
|
+
<option value="newest">${t('organisms.sortNewest') || 'Newest first'}</option>
|
|
802
|
+
</select>` : null}
|
|
803
|
+
<button class="btn-outline btn-sm" disabled=${importingOrg} title=${t('organisms.importOrgHint') || 'Restore an organism from a .zip backup'} onClick=${() => orgFileRef.current && orgFileRef.current.click()}>${'⬆ '}${t('organisms.importOrg') || 'Import'}</button>
|
|
804
|
+
<button class="btn-primary btn-sm" onClick=${() => setShowCreate(true)}>${'+ '}${t('organisms.createNew') || 'Create Organism'}</button>
|
|
805
|
+
</div>
|
|
769
806
|
</div>
|
|
770
807
|
${myOrganisms.length === 0
|
|
771
808
|
? html`<div class="empty">${t('organisms.empty') || 'You are not part of any organisms yet.'}</div>`
|
|
772
|
-
:
|
|
809
|
+
: html`
|
|
810
|
+
<div class="pj-org-list">${sortedMine.map(org => renderOrgRow(org, true))}</div>
|
|
811
|
+
${sortMode === 'custom' && sortedMine.length > 1 ? html`
|
|
812
|
+
<div class="pj-org-hint">${t('organisms.reorderHint') || 'Drag rows to reorder — the order is saved to your profile.'}</div>` : null}
|
|
813
|
+
`
|
|
773
814
|
}
|
|
774
815
|
|
|
775
816
|
<!-- Discover -->
|
|
776
817
|
${publicOrganisms.length > 0 && html`
|
|
777
818
|
<div class="section-title section-title-spaced">${t('organisms.discover') || 'Discover'}</div>
|
|
778
|
-
|
|
819
|
+
<div class="pj-org-list">${publicOrganisms.map(org => renderOrgRow(org, false))}</div>
|
|
779
820
|
`}
|
|
780
821
|
<${ConfirmUI} />
|
|
781
822
|
`;
|
|
782
823
|
}
|
|
783
824
|
|
|
825
|
+
/* ───────────────── Organism home page ─────────────────
|
|
826
|
+
* One home per organism: breadcrumb header (avatar, name, badges, description,
|
|
827
|
+
* Export + Settings) and tabs — Workspaces (the workspace list + content search),
|
|
828
|
+
* Members (invite/approve/roster/blocked), Agents (attach/detach), Board (link to
|
|
829
|
+
* the Boards tab). The Settings panel hosts the edit form, the metadata that used
|
|
830
|
+
* to live in the expanded list card, and the danger zone (leave / delete). */
|
|
831
|
+
function OrganismHome({ org, ghii, showToast, initialSettings, onOpenWs, onBack, onChanged, onLeave, onDelete }) {
|
|
832
|
+
const { confirm, ConfirmUI } = useConfirm();
|
|
833
|
+
const [tab, setTab] = useState(() => { try { return sessionStorage.getItem('aimeat.org.tab') || 'workspaces'; } catch { return 'workspaces'; } });
|
|
834
|
+
const [showSettings, setShowSettings] = useState(!!initialSettings);
|
|
835
|
+
const [wsCount, setWsCount] = useState(null);
|
|
836
|
+
useEffect(() => { try { sessionStorage.setItem('aimeat.org.tab', tab); } catch { /* noop */ } }, [tab]);
|
|
837
|
+
|
|
838
|
+
const isCreator = org.creatorGhii === ghii;
|
|
839
|
+
const isAdmin = org.admins?.includes(ghii);
|
|
840
|
+
const isMember = org.members?.includes(ghii);
|
|
841
|
+
const canEdit = isCreator || isAdmin;
|
|
842
|
+
const typeLabel = t(`organisms.types.${org.type}`) || org.type;
|
|
843
|
+
|
|
844
|
+
/* ── Settings: edit form (admins), metadata, danger zone ── */
|
|
845
|
+
const [editForm, setEditForm] = useState(null);
|
|
846
|
+
const [saving, setSaving] = useState(false);
|
|
847
|
+
// Re-seed the form when another organism is opened (org.id), not on every reload of the same org.
|
|
848
|
+
useEffect(() => { setEditForm(null); }, [org.id]);
|
|
849
|
+
const seedForm = () => ({
|
|
850
|
+
name: org.name || '', description: org.description || '', type: org.type,
|
|
851
|
+
join_policy: org.joinPolicy, visibility: org.visibility, interests: (org.interests || []).join(', '),
|
|
852
|
+
});
|
|
853
|
+
const saveEdit = async () => {
|
|
854
|
+
if (!editForm?.name?.trim()) { showToast(t('organisms.nameRequired') || 'Name is required'); return; }
|
|
855
|
+
setSaving(true);
|
|
856
|
+
try {
|
|
857
|
+
const interests = editForm.interests.split(',').map(s => s.trim()).filter(Boolean);
|
|
858
|
+
const result = await orgService.updateOrganism(org.id, {
|
|
859
|
+
name: editForm.name.trim(), description: editForm.description.trim(),
|
|
860
|
+
type: editForm.type, join_policy: editForm.join_policy, visibility: editForm.visibility, interests,
|
|
861
|
+
});
|
|
862
|
+
if (result?.ok !== false) { showToast(t('organisms.updated') || 'Organism updated'); setEditForm(null); onChanged?.(); }
|
|
863
|
+
else showToast(result?.error?.message || (t('organisms.updateError') || 'Failed to update'));
|
|
864
|
+
} catch { showToast(t('organisms.updateError') || 'Failed to update'); }
|
|
865
|
+
finally { setSaving(false); }
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const boardIdShort = org.boardId && org.boardId.length > 20
|
|
869
|
+
? `${org.boardId.slice(0, 12)}…${org.boardId.slice(-6)}` : (org.boardId || '');
|
|
870
|
+
const copyBoardId = async () => {
|
|
871
|
+
const ok = await copyToClipboard(org.boardId);
|
|
872
|
+
showToast(ok ? (t('organisms.copied') || 'Copied') : (t('organisms.copyFailed') || 'Could not copy'));
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const renderSettings = () => html`
|
|
876
|
+
<div class="card-detail pj-org-settings">
|
|
877
|
+
<div class="detail-grid">
|
|
878
|
+
<div class="detail-item">
|
|
879
|
+
<span class="detail-label">${t('organisms.creator') || 'Creator'}</span>
|
|
880
|
+
<span class="detail-value">${escHtml(org.creatorGhii || '-')}</span>
|
|
881
|
+
</div>
|
|
882
|
+
<div class="detail-item">
|
|
883
|
+
<span class="detail-label">${t('organisms.admins') || 'Admins'}</span>
|
|
884
|
+
<span class="detail-value">${(org.admins || []).join(', ') || '-'}</span>
|
|
885
|
+
</div>
|
|
886
|
+
<div class="detail-item">
|
|
887
|
+
<span class="detail-label">${t('organisms.memberCount') || 'Members'}</span>
|
|
888
|
+
<span class="detail-value">${(org.members || []).length} / ${org.maxMembers || 500}</span>
|
|
889
|
+
</div>
|
|
890
|
+
${org.createdAt ? html`
|
|
891
|
+
<div class="detail-item">
|
|
892
|
+
<span class="detail-label">${t('organisms.createdAt') || 'Created'}</span>
|
|
893
|
+
<span class="detail-value">${fmtDate(org.createdAt)}</span>
|
|
894
|
+
</div>` : null}
|
|
895
|
+
${org.boardId ? html`
|
|
896
|
+
<div class="detail-item">
|
|
897
|
+
<span class="detail-label">${t('organisms.board') || 'Board'}</span>
|
|
898
|
+
<span class="detail-value mono" title=${escHtml(org.boardId)}>${escHtml(boardIdShort)}
|
|
899
|
+
<button class="pj-icon-btn" title=${t('organisms.copyId') || 'Copy ID'} onClick=${copyBoardId}>${'📋'}</button>
|
|
900
|
+
</span>
|
|
901
|
+
</div>` : null}
|
|
902
|
+
</div>
|
|
903
|
+
${(org.interests || []).length > 0 ? html`
|
|
904
|
+
<div class="flex-row-wrap">
|
|
905
|
+
${org.interests.map(tag => html`<span class="file-tag" key=${tag}>${escHtml(tag)}</span>`)}
|
|
906
|
+
</div>` : null}
|
|
907
|
+
|
|
908
|
+
${canEdit ? (editForm ? html`
|
|
909
|
+
<div class="flex-col">
|
|
910
|
+
<input type="text" value=${editForm.name} onInput=${(e) => setEditForm(f => ({ ...f, name: e.target.value }))}
|
|
911
|
+
placeholder=${t('organisms.namePlaceholder') || 'Name'} class="input-field input-sm" />
|
|
912
|
+
<textarea value=${editForm.description} onInput=${(e) => setEditForm(f => ({ ...f, description: e.target.value }))} rows="2"
|
|
913
|
+
placeholder=${t('organisms.descPlaceholder') || 'Description'} class="input-field input-sm" />
|
|
914
|
+
<input type="text" value=${editForm.interests} onInput=${(e) => setEditForm(f => ({ ...f, interests: e.target.value }))}
|
|
915
|
+
placeholder=${t('organisms.interestsPlaceholder') || 'Interests (comma separated)'} class="input-field input-sm" />
|
|
916
|
+
<div class="flex-row-wrap">
|
|
917
|
+
<select value=${editForm.type} onChange=${(e) => setEditForm(f => ({ ...f, type: e.target.value }))} class="input-field input-sm">
|
|
918
|
+
<option value="community">${t('organisms.types.community') || 'Community'}</option>
|
|
919
|
+
<option value="team">${t('organisms.types.team') || 'Team'}</option>
|
|
920
|
+
<option value="club">${t('organisms.types.club') || 'Club'}</option>
|
|
921
|
+
<option value="cooperative">${t('organisms.types.cooperative') || 'Cooperative'}</option>
|
|
922
|
+
<option value="project">${t('organisms.types.project') || 'Project'}</option>
|
|
923
|
+
</select>
|
|
924
|
+
<select value=${editForm.join_policy} onChange=${(e) => setEditForm(f => ({ ...f, join_policy: e.target.value }))} class="input-field input-sm">
|
|
925
|
+
<option value="open">${t('organisms.policyOpen') || 'Open (anyone can join)'}</option>
|
|
926
|
+
<option value="approval_required">${t('organisms.policyApproval') || 'Approval required'}</option>
|
|
927
|
+
<option value="invite_only">${t('organisms.policyInvite') || 'Invite only'}</option>
|
|
928
|
+
</select>
|
|
929
|
+
<select value=${editForm.visibility} onChange=${(e) => setEditForm(f => ({ ...f, visibility: e.target.value }))} class="input-field input-sm">
|
|
930
|
+
<option value="public">${t('organisms.visPublic') || 'Public'}</option>
|
|
931
|
+
<option value="listed">${t('organisms.visListed') || 'Listed'}</option>
|
|
932
|
+
<option value="private">${t('organisms.visPrivate') || 'Private'}</option>
|
|
933
|
+
</select>
|
|
934
|
+
</div>
|
|
935
|
+
<div class="flex-row-wrap">
|
|
936
|
+
<button class="btn-primary btn-sm" onClick=${saveEdit} disabled=${saving}>${saving ? '...' : (t('organisms.save') || 'Save')}</button>
|
|
937
|
+
<button class="btn-ghost btn-sm" onClick=${() => setEditForm(null)}>${t('organisms.cancel') || 'Cancel'}</button>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
` : html`
|
|
941
|
+
<div class="flex-row-wrap">
|
|
942
|
+
<button class="btn-outline btn-sm" onClick=${() => setEditForm(seedForm())}>${t('organisms.edit') || 'Edit'}</button>
|
|
943
|
+
</div>
|
|
944
|
+
`) : null}
|
|
945
|
+
|
|
946
|
+
${(isCreator || isMember) ? html`
|
|
947
|
+
<div class="detail-label">${t('organisms.dangerZone') || 'Danger zone'}</div>
|
|
948
|
+
<div class="flex-row-wrap">
|
|
949
|
+
${isMember && !isCreator ? html`
|
|
950
|
+
<button class="btn-danger btn-sm" onClick=${onLeave}>${t('organisms.leave') || 'Leave'}</button>` : null}
|
|
951
|
+
${isCreator ? html`
|
|
952
|
+
<button class="btn-danger btn-sm" onClick=${onDelete}>${t('organisms.delete') || 'Delete'}</button>` : null}
|
|
953
|
+
</div>` : null}
|
|
954
|
+
</div>`;
|
|
955
|
+
|
|
956
|
+
const tabs = [
|
|
957
|
+
{ id: 'workspaces', label: t('organisms.tabWorkspaces') || 'Workspaces', count: wsCount },
|
|
958
|
+
{ id: 'members', label: t('organisms.tabMembers') || 'Members', count: (org.members || []).length },
|
|
959
|
+
{ id: 'agents', label: t('organisms.tabAgents') || 'Agents', count: (org.agentGaiis || []).length },
|
|
960
|
+
{ id: 'board', label: t('organisms.tabBoard') || 'Board', count: null },
|
|
961
|
+
];
|
|
962
|
+
|
|
963
|
+
return html`
|
|
964
|
+
<div class="pj-ws">
|
|
965
|
+
<div class="pj-org-breadcrumb">
|
|
966
|
+
<button class="pj-org-crumb-link" onClick=${onBack}>${t('organisms.title') || 'Organisms'}</button>
|
|
967
|
+
<span class="pj-org-crumb-sep">/</span>
|
|
968
|
+
<span>${escHtml(org.name || org.id)}</span>
|
|
969
|
+
</div>
|
|
970
|
+
|
|
971
|
+
<div class="pj-org-home-head">
|
|
972
|
+
<div class="pj-org-avatar pj-org-avatar-lg" aria-hidden="true">${orgInitials(org.name)}</div>
|
|
973
|
+
<div class="pj-org-home-title">
|
|
974
|
+
<div class="pj-org-titlerow">
|
|
975
|
+
<span class="pj-org-home-name">${escHtml(org.name || org.id)}</span>
|
|
976
|
+
<span class="badge badge-info">${typeLabel}</span>
|
|
977
|
+
<span class="badge ${org.visibility === 'public' ? 'badge-success' : 'badge-warn'}">${org.visibility || ''}</span>
|
|
978
|
+
</div>
|
|
979
|
+
${org.description ? html`<div class="section-desc pj-org-home-desc">${escHtml(org.description)}</div>` : null}
|
|
980
|
+
</div>
|
|
981
|
+
<div class="pj-org-home-actions">
|
|
982
|
+
<button class="btn-outline btn-sm" title=${t('organisms.exportOrgHint') || 'Download a ZIP backup of the whole organism (all workspaces)'}
|
|
983
|
+
onClick=${() => exportOrganismZip(org, showToast)}>${'⬇ '}${t('organisms.exportOrg') || 'Export'}</button>
|
|
984
|
+
<button class="btn-outline btn-sm ${showSettings ? 'pj-org-btn-active' : ''}" onClick=${() => setShowSettings(s => !s)}>${'⚙ '}${t('organisms.settings') || 'Settings'}</button>
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
|
|
988
|
+
${showSettings ? renderSettings() : null}
|
|
989
|
+
|
|
990
|
+
<div class="pj-org-tabs" role="tablist">
|
|
991
|
+
${tabs.map(tb => html`
|
|
992
|
+
<button class="pj-org-tab ${tab === tb.id ? 'active' : ''}" role="tab" aria-selected=${tab === tb.id} key=${tb.id}
|
|
993
|
+
onClick=${() => setTab(tb.id)}>
|
|
994
|
+
${tb.label}${tb.count !== null && tb.count !== undefined ? html`<span class="pj-org-tab-count">${tb.count}</span>` : null}
|
|
995
|
+
</button>`)}
|
|
996
|
+
</div>
|
|
997
|
+
|
|
998
|
+
${tab === 'workspaces' ? html`
|
|
999
|
+
<${WorkspaceList} org=${org} showToast=${showToast} onOpen=${onOpenWs} onCount=${setWsCount} />
|
|
1000
|
+
<${OrgSearch} orgId=${org.id} onOpenWorkspace=${(ws) => onOpenWs(ws)} />
|
|
1001
|
+
` : null}
|
|
1002
|
+
|
|
1003
|
+
${tab === 'members' ? html`
|
|
1004
|
+
<${OrgMemberManager} org=${org} ghii=${ghii} canManage=${canEdit} isCreator=${isCreator}
|
|
1005
|
+
showToast=${showToast} confirm=${confirm} onChanged=${onChanged} show="members" />` : null}
|
|
1006
|
+
|
|
1007
|
+
${tab === 'agents' ? html`
|
|
1008
|
+
<${OrgMemberManager} org=${org} ghii=${ghii} canManage=${canEdit} isCreator=${isCreator}
|
|
1009
|
+
showToast=${showToast} confirm=${confirm} onChanged=${onChanged} show="agents" />` : null}
|
|
1010
|
+
|
|
1011
|
+
${tab === 'board' ? html`
|
|
1012
|
+
<div class="card-detail">
|
|
1013
|
+
<div class="section-title">${t('organisms.tabBoard') || 'Board'}</div>
|
|
1014
|
+
${org.boardId ? html`
|
|
1015
|
+
<div class="section-desc">${t('organisms.boardDesc') || 'This organism has a discussion board. Open it in the Boards view.'}</div>
|
|
1016
|
+
<div class="pj-org-boardrow">
|
|
1017
|
+
<span class="mono" title=${escHtml(org.boardId)}>${escHtml(boardIdShort)}</span>
|
|
1018
|
+
<button class="pj-icon-btn" title=${t('organisms.copyId') || 'Copy ID'} onClick=${copyBoardId}>${'📋'}</button>
|
|
1019
|
+
<button class="btn-outline btn-sm" onClick=${() => window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'boards' } }))}>
|
|
1020
|
+
${t('organisms.openBoardsTab') || 'Open in Boards'}</button>
|
|
1021
|
+
</div>
|
|
1022
|
+
` : html`
|
|
1023
|
+
<div class="section-desc">${t('organisms.noBoard') || 'This organism has no board.'}</div>
|
|
1024
|
+
`}
|
|
1025
|
+
</div>` : null}
|
|
1026
|
+
|
|
1027
|
+
<${ConfirmUI} />
|
|
1028
|
+
</div>`;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
784
1031
|
/* ───────────────── Organism workspace (manifest-driven) ─────────────────
|
|
785
1032
|
* Any organism can have a governed workspace. If it has no manifest yet, offer
|
|
786
1033
|
* "Set up workspace" (applies the project template). Otherwise render the
|
|
@@ -792,7 +1039,9 @@ const PRIMARY_FIELD = { goal: 'title', plan: 'approach', deliverable: 'title', r
|
|
|
792
1039
|
/* Workspace list — an organism contains many workspaces (each an independent manifest + data set,
|
|
793
1040
|
* namespaced under organism.{id}.w.{wsId}.*). Lists the registry (organism.{id}.meta.workspaces),
|
|
794
1041
|
* lets the user create a new one (→ opens its setup/generate screen) or open/delete an existing one. */
|
|
795
|
-
|
|
1042
|
+
/* Rendered embedded in OrganismHome's Workspaces tab (the back/export topbar + organism title
|
|
1043
|
+
* moved to the home header). onCount reports the discovered workspace count for the tab badge. */
|
|
1044
|
+
function WorkspaceList({ org, showToast, onOpen, onCount }) {
|
|
796
1045
|
const orgId = org.id;
|
|
797
1046
|
const { confirm, ConfirmUI } = useConfirm();
|
|
798
1047
|
const [list, setList] = useState(null); // null = loading
|
|
@@ -806,7 +1055,11 @@ function WorkspaceList({ org, showToast, onOpen, onBack }) {
|
|
|
806
1055
|
|
|
807
1056
|
// Discovery: every workspace in the org (membership-gated) with this user's access status. A member
|
|
808
1057
|
// sees workspaces they can't yet read (access:'none') so they can request access.
|
|
809
|
-
const load = useCallback(async () => {
|
|
1058
|
+
const load = useCallback(async () => {
|
|
1059
|
+
const l = await orgService.discoverWorkspaces(orgId);
|
|
1060
|
+
setList(l);
|
|
1061
|
+
onCount?.(Array.isArray(l) ? l.length : 0);
|
|
1062
|
+
}, [orgId, onCount]);
|
|
810
1063
|
useEffect(() => { load(); }, [load]);
|
|
811
1064
|
|
|
812
1065
|
const requestAccess = async (w) => {
|
|
@@ -890,27 +1143,10 @@ function WorkspaceList({ org, showToast, onOpen, onBack }) {
|
|
|
890
1143
|
} catch (e) { showToast((e && e.message) || 'Import failed'); }
|
|
891
1144
|
finally { setBusy(false); }
|
|
892
1145
|
};
|
|
893
|
-
const doExportOrg = async () => {
|
|
894
|
-
try {
|
|
895
|
-
const res = await fetch(`/v1/organisms/${encodeURIComponent(orgId)}/export`, { headers: { Authorization: 'Bearer ' + jwt() } });
|
|
896
|
-
if (!res.ok) throw new Error('Export failed');
|
|
897
|
-
const blob = await res.blob();
|
|
898
|
-
const url = URL.createObjectURL(blob);
|
|
899
|
-
const a = document.createElement('a');
|
|
900
|
-
a.href = url; a.download = `organism-${String(org.name || orgId).replace(/[^a-z0-9_-]+/gi, '-').slice(0, 40)}.zip`;
|
|
901
|
-
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
|
|
902
|
-
} catch (e) { showToast((e && e.message) || 'Export failed'); }
|
|
903
|
-
};
|
|
904
|
-
|
|
905
1146
|
return html`
|
|
906
|
-
<div class="pj-ws">
|
|
1147
|
+
<div class="pj-ws-embedded">
|
|
907
1148
|
<${ConfirmUI} />
|
|
908
1149
|
<input type="file" accept=".zip,application/zip" ref=${fileRef} style="display:none" onChange=${(e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; doImport(f); }} />
|
|
909
|
-
<div class="pj-ws-topbar">
|
|
910
|
-
<button class="btn-ghost btn-sm" onClick=${onBack}>${'← '}${t('organisms.allOrganisms') || 'All organisms'}</button>
|
|
911
|
-
<button class="btn-ghost btn-sm" title=${t('organisms.exportOrgHint') || 'Download a ZIP backup of the whole organism (all workspaces)'} onClick=${doExportOrg}>${'⬇ '}${t('organisms.exportOrg') || 'Export organism'}</button>
|
|
912
|
-
</div>
|
|
913
|
-
<div class="section-title">${escHtml(org.name || 'Organism')}</div>
|
|
914
1150
|
<div class="section-desc">${t('organisms.workspacesDesc') || 'Workspaces in this organism — each is an independent space with its own documents, records and history.'}</div>
|
|
915
1151
|
|
|
916
1152
|
${overview ? html`
|