aimeat 1.25.0 → 1.26.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/README.md +8 -1
- package/dist/locales/en.json +144 -0
- package/dist/locales/fi.json +144 -0
- package/dist/public/components/ImageDeliverable.js +2 -1
- package/dist/public/components/Modal.js +3 -2
- package/dist/public/components/NotificationBell.js +2 -1
- package/dist/public/components/PresenceDot.js +53 -0
- package/dist/public/css/components/markdown.css +67 -0
- package/dist/public/css/theme.css +30 -0
- package/dist/public/css/views/notebook.css +195 -0
- package/dist/public/css/views/profile.css +15 -0
- package/dist/public/js/api.js +5 -3
- package/dist/public/js/components/auth-image.js +3 -2
- package/dist/public/js/components/tag-cloud.js +1 -1
- package/dist/public/js/components/tag-editor.js +1 -1
- package/dist/public/js/hooks.js +1 -2
- package/dist/public/js/presence-store.js +97 -0
- package/dist/public/js/services/admin.js +14 -5
- package/dist/public/js/services/boards.js +1 -1
- package/dist/public/js/services/ecosystem.js +1 -1
- package/dist/public/js/services/foundry-prompts-base.js +2 -2
- package/dist/public/js/services/foundry-smoke.js +2 -1
- package/dist/public/js/services/foundry-validate.js +6 -8
- package/dist/public/js/services/foundry.js +4 -3
- package/dist/public/js/services/generator-prompts-base.js +2 -2
- package/dist/public/js/services/generator-smoke.js +2 -1
- package/dist/public/js/services/generator-validate.js +5 -7
- package/dist/public/js/services/generator.js +2 -1
- package/dist/public/js/services/memory.js +13 -0
- package/dist/public/js/services/messages.js +2 -1
- package/dist/public/js/services/notebook.js +124 -0
- package/dist/public/js/services/packages.js +3 -2
- package/dist/public/js/services/presence.js +35 -0
- package/dist/public/js/style-testbed.js +6 -5
- package/dist/public/lib/minidenticons.min.js +1 -0
- package/dist/public/lib/yaml.mjs +1 -0
- package/dist/public/spa.html +90 -8
- package/dist/public/types/aimeat-globals.d.ts +52 -0
- package/dist/public/types/vendored-libs.d.ts +56 -0
- package/dist/public/views/_template.js +3 -1
- package/dist/public/views/admin/agent-integration-tab.js +4 -3
- package/dist/public/views/admin/agents-tab.js +3 -2
- package/dist/public/views/admin/config-tab.js +4 -3
- package/dist/public/views/admin/economy-tab.js +4 -3
- package/dist/public/views/admin/federation-tab.js +138 -1
- package/dist/public/views/admin/maintenance-tab.js +1 -1
- package/dist/public/views/admin/messages-tab.js +115 -0
- package/dist/public/views/admin/overview-tab.js +4 -3
- package/dist/public/views/admin/portal-tab.js +90 -2
- package/dist/public/views/admin/push-tab.js +8 -6
- package/dist/public/views/admin/security-tab.js +7 -7
- package/dist/public/views/admin.js +29 -9
- package/dist/public/views/doc-solo.js +3 -1
- package/dist/public/views/guides.js +8 -4
- package/dist/public/views/hobbies.js +4 -2
- package/dist/public/views/marketplace.js +10 -9
- package/dist/public/views/openclaw.js +1 -1
- package/dist/public/views/portal-dev.js +5 -2
- package/dist/public/views/portal.js +3 -2
- package/dist/public/views/profile/agents/shared-board.js +4 -2
- package/dist/public/views/profile/agents/tab-messages.js +13 -11
- package/dist/public/views/profile/agents-messages-subtab.js +2 -1
- package/dist/public/views/profile/apps-tab.js +2 -1
- package/dist/public/views/profile/boards-tab.js +3 -2
- package/dist/public/views/profile/calibrator-batch.js +3 -2
- package/dist/public/views/profile/data-wallet-tab.js +2 -1
- package/dist/public/views/profile/extensions-tab.js +4 -3
- package/dist/public/views/profile/federation-tab.js +4 -0
- package/dist/public/views/profile/foundry-dashboard/use-autopilot.js +3 -2
- package/dist/public/views/profile/foundry-detail.js +1 -1
- package/dist/public/views/profile/generator-detail.js +1 -1
- package/dist/public/views/profile/inbox-tab.js +7 -3
- package/dist/public/views/profile/inline-panels.js +3 -1
- package/dist/public/views/profile/knowledge-tab.js +2 -1
- package/dist/public/views/profile/landing-page.js +88 -0
- package/dist/public/views/profile/memory-tab.js +6 -5
- package/dist/public/views/profile/notebook-tab.js +500 -0
- package/dist/public/views/profile/organisms/activity-panel.js +135 -0
- package/dist/public/views/profile/organisms/agents.js +141 -0
- package/dist/public/views/profile/organisms/document.js +274 -0
- package/dist/public/views/profile/organisms/helpers.js +61 -0
- package/dist/public/views/profile/organisms/home.js +315 -0
- package/dist/public/views/profile/organisms/members.js +220 -0
- package/dist/public/views/profile/organisms/panels.js +173 -0
- package/dist/public/views/profile/organisms/participants-panel.js +160 -0
- package/dist/public/views/profile/organisms/schema-form.js +127 -0
- package/dist/public/views/profile/organisms/sources-panel.js +164 -0
- package/dist/public/views/profile/organisms/widgets.js +58 -0
- package/dist/public/views/profile/organisms/workspace-comments.js +99 -0
- package/dist/public/views/profile/organisms/workspace-list.js +268 -0
- package/dist/public/views/profile/organisms/workspace.js +1272 -0
- package/dist/public/views/profile/organisms-tab.js +456 -3819
- package/dist/public/views/profile/shared.js +64 -2
- package/dist/public/views/profile/workflows-form.js +1 -1
- package/dist/public/views/profile.js +16 -14
- package/dist/public/views/public-knowledge-viewer.js +3 -2
- package/dist/public/views/public-workspace-viewer.js +2 -1
- package/dist/scripts/check-importmap.d.ts +2 -0
- package/dist/scripts/check-importmap.d.ts.map +1 -0
- package/dist/scripts/check-importmap.js +102 -0
- package/dist/scripts/check-importmap.js.map +1 -0
- package/dist/src/cli/scaffold.d.ts +26 -3
- package/dist/src/cli/scaffold.d.ts.map +1 -1
- package/dist/src/cli/scaffold.js +33 -3
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +5 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +927 -11
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +32 -6
- package/dist/src/generated/prisma-postgres/index-browser.js +29 -3
- package/dist/src/generated/prisma-postgres/index.d.ts +1970 -160
- package/dist/src/generated/prisma-postgres/index.js +32 -6
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +45 -12
- package/dist/src/generated/prisma-postgres/wasm.js +32 -6
- package/dist/src/index.js +36 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes/admin-monitoring.d.ts.map +1 -1
- package/dist/src/routes/admin-monitoring.js +8 -0
- package/dist/src/routes/admin-monitoring.js.map +1 -1
- package/dist/src/routes/federation-peer.d.ts.map +1 -1
- package/dist/src/routes/federation-peer.js +327 -43
- package/dist/src/routes/federation-peer.js.map +1 -1
- package/dist/src/routes/federation-sync.d.ts +2 -0
- package/dist/src/routes/federation-sync.d.ts.map +1 -1
- package/dist/src/routes/federation-sync.js +20 -1
- package/dist/src/routes/federation-sync.js.map +1 -1
- package/dist/src/routes/librarian.d.ts +18 -0
- package/dist/src/routes/librarian.d.ts.map +1 -0
- package/dist/src/routes/librarian.js +88 -0
- package/dist/src/routes/librarian.js.map +1 -0
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +2 -1
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +9 -1
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/presence.d.ts +23 -0
- package/dist/src/routes/presence.d.ts.map +1 -0
- package/dist/src/routes/presence.js +88 -0
- package/dist/src/routes/presence.js.map +1 -0
- package/dist/src/routes/site.d.ts +12 -0
- package/dist/src/routes/site.d.ts.map +1 -1
- package/dist/src/routes/site.js +45 -1
- package/dist/src/routes/site.js.map +1 -1
- package/dist/src/routes/sse.d.ts +2 -0
- package/dist/src/routes/sse.d.ts.map +1 -1
- package/dist/src/routes/sse.js +9 -0
- package/dist/src/routes/sse.js.map +1 -1
- package/dist/src/routes/wellknown.d.ts.map +1 -1
- package/dist/src/routes/wellknown.js +5 -0
- package/dist/src/routes/wellknown.js.map +1 -1
- package/dist/src/routes/work.d.ts.map +1 -1
- package/dist/src/routes/work.js +6 -0
- package/dist/src/routes/work.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +13 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +34 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +2 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/federation-availability.d.ts +28 -0
- package/dist/src/services/federation-availability.d.ts.map +1 -0
- package/dist/src/services/federation-availability.js +58 -0
- package/dist/src/services/federation-availability.js.map +1 -0
- package/dist/src/services/federation-book.d.ts +70 -0
- package/dist/src/services/federation-book.d.ts.map +1 -0
- package/dist/src/services/federation-book.js +198 -0
- package/dist/src/services/federation-book.js.map +1 -0
- package/dist/src/services/federation-tiers.d.ts +45 -0
- package/dist/src/services/federation-tiers.d.ts.map +1 -0
- package/dist/src/services/federation-tiers.js +58 -0
- package/dist/src/services/federation-tiers.js.map +1 -0
- package/dist/src/services/federation.d.ts +15 -0
- package/dist/src/services/federation.d.ts.map +1 -1
- package/dist/src/services/federation.js +12 -0
- package/dist/src/services/federation.js.map +1 -1
- package/dist/src/services/librarian.d.ts +62 -0
- package/dist/src/services/librarian.d.ts.map +1 -0
- package/dist/src/services/librarian.js +135 -0
- package/dist/src/services/librarian.js.map +1 -0
- package/dist/src/services/message-delivery.d.ts +3 -1
- package/dist/src/services/message-delivery.d.ts.map +1 -1
- package/dist/src/services/message-delivery.js +23 -2
- package/dist/src/services/message-delivery.js.map +1 -1
- package/dist/src/services/network-policy.d.ts +88 -0
- package/dist/src/services/network-policy.d.ts.map +1 -0
- package/dist/src/services/network-policy.js +154 -0
- package/dist/src/services/network-policy.js.map +1 -0
- package/dist/src/services/notebook-classify-prompt.d.ts +15 -0
- package/dist/src/services/notebook-classify-prompt.d.ts.map +1 -0
- package/dist/src/services/notebook-classify-prompt.js +39 -0
- package/dist/src/services/notebook-classify-prompt.js.map +1 -0
- package/dist/src/services/notebook-classify.d.ts +80 -0
- package/dist/src/services/notebook-classify.d.ts.map +1 -0
- package/dist/src/services/notebook-classify.js +150 -0
- package/dist/src/services/notebook-classify.js.map +1 -0
- package/dist/src/services/presence.d.ts +108 -0
- package/dist/src/services/presence.d.ts.map +1 -0
- package/dist/src/services/presence.js +304 -0
- package/dist/src/services/presence.js.map +1 -0
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +13 -0
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/site.d.ts +20 -0
- package/dist/src/services/site.d.ts.map +1 -1
- package/dist/src/services/site.js +70 -0
- package/dist/src/services/site.js.map +1 -1
- package/dist/src/storage/interface.d.ts +48 -0
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +11 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +128 -2
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +7 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +27 -2
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +5 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.js +48 -0
- package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts +2 -0
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.js +55 -0
- package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +61 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts +5 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/memory.repository.d.ts +25 -0
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/dist/src/utils/node-descriptor.d.ts +34 -0
- package/dist/src/utils/node-descriptor.d.ts.map +1 -0
- package/dist/src/utils/node-descriptor.js +42 -0
- package/dist/src/utils/node-descriptor.js.map +1 -0
- package/dist/src/utils/version.d.ts +3 -0
- package/dist/src/utils/version.d.ts.map +1 -0
- package/dist/src/utils/version.js +40 -0
- package/dist/src/utils/version.js.map +1 -0
- package/dist/static/app-catalog.html +643 -4
- package/package.json +5 -2
- package/prisma/schema.postgres.prisma +51 -18
- package/prisma/schema.prisma +29 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file home.js
|
|
3
|
+
* @description Organism home page — breadcrumb header (avatar, name, badges, description, Export +
|
|
4
|
+
* Settings) and tabs (Workspaces / Members / Agents / Board). The Settings panel hosts the
|
|
5
|
+
* labelled+grouped edit form, the read-only metadata line, and the danger zone (leave / delete).
|
|
6
|
+
* Extracted from organisms-tab.js with no behaviour change.
|
|
7
|
+
* @structure OrganismHome
|
|
8
|
+
* @usage import { OrganismHome } from '/views/profile/organisms/home.js';
|
|
9
|
+
* @version-history
|
|
10
|
+
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
11
|
+
*/
|
|
12
|
+
import { h } from 'preact';
|
|
13
|
+
import { useState, useEffect, useMemo, useRef } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import { TagInput } from '/views/profile/shared.js';
|
|
18
|
+
import { useConfirm } from '/components/Modal.js';
|
|
19
|
+
import * as orgService from '/js/services/organisms.js';
|
|
20
|
+
import { copyToClipboard } from '/js/utils.js';
|
|
21
|
+
import { recordRecent } from '/js/recents.js';
|
|
22
|
+
import { fmtDate, orgInitials, exportOrganismZip } from '/views/profile/organisms/helpers.js';
|
|
23
|
+
import { StructureOverview } from '/views/profile/organisms/widgets.js';
|
|
24
|
+
import { WorkspaceList } from '/views/profile/organisms/workspace-list.js';
|
|
25
|
+
import { OrgMemberManager } from '/views/profile/organisms/members.js';
|
|
26
|
+
import { OrgAgentsPanel } from '/views/profile/organisms/agents.js';
|
|
27
|
+
import { BoardPreview } from '/views/profile/organisms/panels.js';
|
|
28
|
+
|
|
29
|
+
/* ───────────────── Organism home page ─────────────────
|
|
30
|
+
* One home per organism: breadcrumb header (avatar, name, badges, description,
|
|
31
|
+
* Export + Settings) and tabs — Workspaces (the workspace list + content search),
|
|
32
|
+
* Members (invite/approve/roster/blocked), Agents (attach/detach), Board (link to
|
|
33
|
+
* the Boards tab). The Settings panel hosts the edit form, the metadata that used
|
|
34
|
+
* to live in the expanded list card, and the danger zone (leave / delete). */
|
|
35
|
+
export function OrganismHome({ org, ghii, showToast, initialSettings, onOpenWs, onBack, onChanged, onLeave }) {
|
|
36
|
+
const { confirm, ConfirmUI } = useConfirm();
|
|
37
|
+
const [tab, setTab] = useState(() => { try { return sessionStorage.getItem('aimeat.org.tab') || 'workspaces'; } catch { return 'workspaces'; } });
|
|
38
|
+
const [showSettings, setShowSettings] = useState(!!initialSettings);
|
|
39
|
+
const [wsCount, setWsCount] = useState(null);
|
|
40
|
+
const [pendingJoin, setPendingJoin] = useState(0); // Members tab pill — visible without opening the tab
|
|
41
|
+
useEffect(() => { try { sessionStorage.setItem('aimeat.org.tab', tab); } catch { /* noop */ } }, [tab]);
|
|
42
|
+
|
|
43
|
+
const isCreator = org.creatorGhii === ghii;
|
|
44
|
+
const isAdmin = org.admins?.includes(ghii);
|
|
45
|
+
const isMember = org.members?.includes(ghii);
|
|
46
|
+
const canEdit = isCreator || isAdmin;
|
|
47
|
+
const typeLabel = t(`organisms.types.${org.type}`) || org.type;
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!canEdit) return undefined;
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
const fetchIt = () => orgService.listJoinRequests(org.id)
|
|
53
|
+
.then(r => { if (!cancelled) setPendingJoin(((r?.data?.join_requests) || []).filter(x => x.status === 'pending').length); })
|
|
54
|
+
.catch(() => {});
|
|
55
|
+
fetchIt();
|
|
56
|
+
window.addEventListener('aimeat-live-update', fetchIt);
|
|
57
|
+
return () => { cancelled = true; window.removeEventListener('aimeat-live-update', fetchIt); };
|
|
58
|
+
}, [org.id, canEdit]);
|
|
59
|
+
|
|
60
|
+
// Feed the home page's "Continue" list (only once the real name is known, not the {id} stub).
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (org.name) recordRecent({ type: 'organism', id: org.id, label: org.name, data: { orgId: org.id } });
|
|
63
|
+
}, [org.id, org.name]);
|
|
64
|
+
|
|
65
|
+
/* ── Settings: labelled + grouped form (Identity / Access) with live dirty-check, a one-line
|
|
66
|
+
* read-only metadata row, per-choice access hints, and a clearly separated danger zone whose
|
|
67
|
+
* delete needs the organism's name typed and states exactly what gets removed. ── */
|
|
68
|
+
const baseline = useMemo(() => ({
|
|
69
|
+
name: org.name || '', description: org.description || '', type: org.type || 'community',
|
|
70
|
+
join_policy: org.joinPolicy || 'open', visibility: org.visibility || 'public',
|
|
71
|
+
interests: [...(org.interests || [])],
|
|
72
|
+
}), [org]);
|
|
73
|
+
const [form, setForm] = useState(baseline);
|
|
74
|
+
const [saving, setSaving] = useState(false);
|
|
75
|
+
// Sync the form whenever fresher org data arrives (e.g. the home mounted from an F5 with only a
|
|
76
|
+
// {id} stub before the list loaded) — but ONLY while the user hasn't touched the fields, so a
|
|
77
|
+
// live-update reload never clobbers typing.
|
|
78
|
+
const prevBaselineRef = useRef(baseline);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const prev = prevBaselineRef.current;
|
|
81
|
+
const untouched = form.name === prev.name && form.description === prev.description
|
|
82
|
+
&& form.type === prev.type && form.join_policy === prev.join_policy
|
|
83
|
+
&& form.visibility === prev.visibility && form.interests.join(' ') === prev.interests.join(' ');
|
|
84
|
+
if (untouched) setForm(baseline);
|
|
85
|
+
prevBaselineRef.current = baseline;
|
|
86
|
+
}, [baseline]); const dirty = form.name !== baseline.name || form.description !== baseline.description
|
|
87
|
+
|| form.type !== baseline.type || form.join_policy !== baseline.join_policy
|
|
88
|
+
|| form.visibility !== baseline.visibility
|
|
89
|
+
|| form.interests.join(' ') !== baseline.interests.join(' ');
|
|
90
|
+
// Save doubles as the unsaved-changes indicator: enabled ⇔ something actually changed.
|
|
91
|
+
const saveEdit = async () => {
|
|
92
|
+
if (!form.name.trim()) { showToast(t('organisms.nameRequired') || 'Name is required'); return; }
|
|
93
|
+
setSaving(true);
|
|
94
|
+
try {
|
|
95
|
+
const result = await orgService.updateOrganism(org.id, {
|
|
96
|
+
name: form.name.trim(), description: form.description.trim(),
|
|
97
|
+
type: form.type, join_policy: form.join_policy, visibility: form.visibility, interests: form.interests,
|
|
98
|
+
});
|
|
99
|
+
if (result?.ok !== false) { showToast(t('organisms.updated') || 'Organism updated'); onChanged?.(); }
|
|
100
|
+
else showToast(result?.error?.message || (t('organisms.updateError') || 'Failed to update'));
|
|
101
|
+
} catch { showToast(t('organisms.updateError') || 'Failed to update'); }
|
|
102
|
+
finally { setSaving(false); }
|
|
103
|
+
};
|
|
104
|
+
// Leaving a dirty form (closing settings / breadcrumb back) asks before dropping the changes.
|
|
105
|
+
const guardDirty = (fn) => {
|
|
106
|
+
if (showSettings && dirty) confirm(t('organisms.discardChanges') || 'Discard unsaved changes?', () => { setForm(baseline); fn(); }, { danger: true });
|
|
107
|
+
else fn();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const boardIdShort = org.boardId && org.boardId.length > 20
|
|
111
|
+
? `${org.boardId.slice(0, 12)}…${org.boardId.slice(-6)}` : (org.boardId || '');
|
|
112
|
+
const copyBoardId = async () => {
|
|
113
|
+
const ok = await copyToClipboard(org.boardId);
|
|
114
|
+
showToast(ok ? (t('organisms.copied') || 'Copied') : (t('organisms.copyFailed') || 'Could not copy'));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// What a delete actually removes (counted from the accessible workspaces when settings opens).
|
|
118
|
+
const [delStats, setDelStats] = useState(null); // { ws, recs, docs }
|
|
119
|
+
const [delOpen, setDelOpen] = useState(false);
|
|
120
|
+
const [delName, setDelName] = useState('');
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!showSettings || !isCreator) return undefined;
|
|
123
|
+
let cancelled = false;
|
|
124
|
+
(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const wss = (await orgService.discoverWorkspaces(org.id)).filter(w => w.access !== 'none');
|
|
127
|
+
let recs = 0, docs = 0;
|
|
128
|
+
await Promise.all(wss.map(async (w) => {
|
|
129
|
+
const wsData = await orgService.getWorkspace(org.id, w.id).catch(() => null);
|
|
130
|
+
for (const ot of (wsData?.manifest?.objectTypes || []).filter(orgService.isMemorySpace)) {
|
|
131
|
+
const n = new Set([...(wsData.drafts?.[ot.name] || []), ...(wsData.objects?.[ot.name] || [])].map(d => d.id)).size;
|
|
132
|
+
if (orgService.isDocSpace(ot)) docs += n; else recs += n;
|
|
133
|
+
}
|
|
134
|
+
}));
|
|
135
|
+
if (!cancelled) setDelStats({ ws: wss.length, recs, docs });
|
|
136
|
+
} catch { /* the counts are a courtesy — the generic warning still shows */ }
|
|
137
|
+
})();
|
|
138
|
+
return () => { cancelled = true; };
|
|
139
|
+
}, [org.id, showSettings, isCreator]);
|
|
140
|
+
const delStatsText = delStats
|
|
141
|
+
? (t('organisms.deleteOrganismStats') || 'Deletes {w} workspaces, {r} records and {d} documents. This cannot be undone.')
|
|
142
|
+
.replace('{w}', String(delStats.ws)).replace('{r}', String(delStats.recs)).replace('{d}', String(delStats.docs))
|
|
143
|
+
: (t('organisms.deleteWarnGeneric') || 'Deletes the organism with all its workspaces and content. This cannot be undone.');
|
|
144
|
+
const doDelete = () => {
|
|
145
|
+
confirm(`${(t('organisms.confirmDeleteName') || 'Delete “{name}”?').replace('{name}', org.name || org.id)} ${delStatsText}`, async () => {
|
|
146
|
+
try {
|
|
147
|
+
await orgService.deleteOrganism(org.id);
|
|
148
|
+
showToast(t('organisms.deleted') || 'Organism deleted');
|
|
149
|
+
onBack(); onChanged?.();
|
|
150
|
+
} catch { showToast(t('organisms.deleteError') || 'Failed to delete'); }
|
|
151
|
+
}, { danger: true, title: t('organisms.deleteOrganismTitle') || 'Delete this organism' });
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const extraAdmins = (org.admins || []).filter(a => a !== org.creatorGhii);
|
|
155
|
+
const visHint = t(`organisms.visHint.${form.visibility}`);
|
|
156
|
+
const policyHint = t(`organisms.policyHint.${form.join_policy}`);
|
|
157
|
+
const hintText = [visHint, policyHint].filter(h => h && !h.startsWith('organisms.')).join(' ');
|
|
158
|
+
|
|
159
|
+
const renderSettings = () => html`
|
|
160
|
+
<div class="card-detail pj-org-settings">
|
|
161
|
+
<div class="pj-meta-line">
|
|
162
|
+
${org.createdAt ? html`<span>${t('organisms.createdAt') || 'Created'} ${fmtDate(org.createdAt)}</span>` : null}
|
|
163
|
+
<span>${t('organisms.creator') || 'Creator'} ${(org.creatorGhii || '-')}</span>
|
|
164
|
+
${extraAdmins.length > 0 ? html`<span>${t('organisms.admins') || 'Admins'} ${(extraAdmins.join(', '))}</span>` : null}
|
|
165
|
+
${org.boardId ? html`
|
|
166
|
+
<span>${t('organisms.board') || 'Board'} <span class="mono" title=${(org.boardId)}>${(boardIdShort)}</span>
|
|
167
|
+
<button class="pj-icon-btn" title=${t('organisms.copyId') || 'Copy ID'} onClick=${copyBoardId}>${'📋'}</button>
|
|
168
|
+
</span>` : null}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
${canEdit ? html`
|
|
172
|
+
<div class="pj-form-group">${t('organisms.formIdentity') || 'Identity'}</div>
|
|
173
|
+
<label class="pj-field"><span>${t('organisms.fieldName') || 'Name'}</span>
|
|
174
|
+
<input type="text" class="input-field input-sm" value=${form.name} onInput=${(e) => setForm(f => ({ ...f, name: e.target.value }))} /></label>
|
|
175
|
+
<label class="pj-field"><span>${t('organisms.fieldDescription') || 'Description'}</span>
|
|
176
|
+
<textarea class="input-field input-sm" rows="2" value=${form.description} onInput=${(e) => setForm(f => ({ ...f, description: e.target.value }))}></textarea></label>
|
|
177
|
+
<div class="pj-field"><span>${t('organisms.fieldInterests') || 'Interests'}</span>
|
|
178
|
+
<${TagInput} tags=${form.interests} onChange=${(tags) => setForm(f => ({ ...f, interests: tags }))} placeholder=${t('organisms.addTag') || 'Add…'} /></div>
|
|
179
|
+
|
|
180
|
+
<div class="pj-form-group">${t('organisms.formAccess') || 'Access'}</div>
|
|
181
|
+
<div class="pj-form-row">
|
|
182
|
+
<label class="pj-field"><span>${t('organisms.fieldType') || 'Type'}</span>
|
|
183
|
+
<select class="input-field input-sm" value=${form.type} onChange=${(e) => setForm(f => ({ ...f, type: e.target.value }))}>
|
|
184
|
+
<option value="community">${t('organisms.types.community') || 'Community'}</option>
|
|
185
|
+
<option value="team">${t('organisms.types.team') || 'Team'}</option>
|
|
186
|
+
<option value="club">${t('organisms.types.club') || 'Club'}</option>
|
|
187
|
+
<option value="cooperative">${t('organisms.types.cooperative') || 'Cooperative'}</option>
|
|
188
|
+
<option value="project">${t('organisms.types.project') || 'Project'}</option>
|
|
189
|
+
</select></label>
|
|
190
|
+
<label class="pj-field"><span>${t('organisms.policyLabel') || 'Join policy'}</span>
|
|
191
|
+
<select class="input-field input-sm" value=${form.join_policy} onChange=${(e) => setForm(f => ({ ...f, join_policy: e.target.value }))}>
|
|
192
|
+
<option value="open">${t('organisms.policyOpen') || 'Open (anyone can join)'}</option>
|
|
193
|
+
<option value="approval_required">${t('organisms.policyApproval') || 'Approval required'}</option>
|
|
194
|
+
<option value="invite_only">${t('organisms.policyInvite') || 'Invite only'}</option>
|
|
195
|
+
</select></label>
|
|
196
|
+
<label class="pj-field"><span>${t('organisms.fieldVisibility') || 'Visibility'}</span>
|
|
197
|
+
<select class="input-field input-sm" value=${form.visibility} onChange=${(e) => setForm(f => ({ ...f, visibility: e.target.value }))}>
|
|
198
|
+
<option value="public">${t('organisms.visPublic') || 'Public'}</option>
|
|
199
|
+
<option value="listed">${t('organisms.visListed') || 'Listed'}</option>
|
|
200
|
+
<option value="private">${t('organisms.visPrivate') || 'Private'}</option>
|
|
201
|
+
</select></label>
|
|
202
|
+
</div>
|
|
203
|
+
${hintText ? html`<div class="pj-form-hint">${hintText}</div>` : null}
|
|
204
|
+
|
|
205
|
+
<div class="form-actions">
|
|
206
|
+
<button class="btn-primary btn-sm" onClick=${saveEdit} disabled=${saving || !dirty || !form.name.trim()}>
|
|
207
|
+
${saving ? '...' : (t('organisms.saveChanges') || 'Save changes')}</button>
|
|
208
|
+
<button class="btn-ghost btn-sm" onClick=${() => { setForm(baseline); setShowSettings(false); }}>${t('organisms.cancel') || 'Cancel'}</button>
|
|
209
|
+
</div>
|
|
210
|
+
` : null}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
${(isCreator || isMember) ? html`
|
|
214
|
+
<div class="pj-danger pj-danger-box">
|
|
215
|
+
${isCreator ? html`
|
|
216
|
+
<div class="pj-danger-row">
|
|
217
|
+
<div class="pj-danger-text">
|
|
218
|
+
<div class="pj-danger-title">${t('organisms.deleteOrganismTitle') || 'Delete this organism'}</div>
|
|
219
|
+
<div class="pj-danger-sub">${delStatsText}</div>
|
|
220
|
+
</div>
|
|
221
|
+
<button class="btn-danger btn-sm" onClick=${() => { setDelOpen(o => !o); setDelName(''); }}>${t('organisms.deleteDots') || 'Delete…'}</button>
|
|
222
|
+
</div>
|
|
223
|
+
${delOpen ? html`
|
|
224
|
+
<div class="pj-danger-confirm">
|
|
225
|
+
<label class="pj-field"><span>${(t('organisms.confirmTypeName') || 'Type the organism’s name to confirm') + ': ' + (org.name || '')}</span>
|
|
226
|
+
<input type="text" class="input-field input-sm" value=${delName} onInput=${(e) => setDelName(e.target.value)} placeholder=${org.name || ''} /></label>
|
|
227
|
+
<button class="btn-danger btn-sm" disabled=${delName.trim() !== (org.name || '').trim()} onClick=${doDelete}>${t('organisms.delete') || 'Delete'}</button>
|
|
228
|
+
</div>` : null}
|
|
229
|
+
` : null}
|
|
230
|
+
${isMember && !isCreator ? html`
|
|
231
|
+
<div class="pj-danger-row">
|
|
232
|
+
<div class="pj-danger-text">
|
|
233
|
+
<div class="pj-danger-title">${t('organisms.leave') || 'Leave'}</div>
|
|
234
|
+
</div>
|
|
235
|
+
<button class="btn-danger btn-sm" onClick=${onLeave}>${t('organisms.leave') || 'Leave'}</button>
|
|
236
|
+
</div>` : null}
|
|
237
|
+
</div>` : null}
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
const reqPill = pendingJoin > 0
|
|
241
|
+
? (pendingJoin === 1 ? (t('organisms.reqOne') || '1 request') : (t('organisms.reqMany') || '{n} requests').replace('{n}', String(pendingJoin)))
|
|
242
|
+
: null;
|
|
243
|
+
const tabs = [
|
|
244
|
+
{ id: 'workspaces', label: t('organisms.tabWorkspaces') || 'Workspaces', count: wsCount },
|
|
245
|
+
{ id: 'members', label: t('organisms.tabMembers') || 'Members', count: (org.members || []).length, pill: reqPill },
|
|
246
|
+
{ id: 'agents', label: t('organisms.tabAgents') || 'Agents', count: (org.agentGaiis || []).length },
|
|
247
|
+
{ id: 'board', label: t('organisms.tabBoard') || 'Board', count: null },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
// Settings REPLACES the tab content — while it is open no tab is highlighted (a lit tab above
|
|
251
|
+
// settings content lies about what the user is looking at), and the breadcrumb says so.
|
|
252
|
+
const activeHomeTab = showSettings ? '' : tab;
|
|
253
|
+
const pickHomeTab = (id) => guardDirty(() => { setShowSettings(false); setTab(id); });
|
|
254
|
+
|
|
255
|
+
return html`
|
|
256
|
+
<div class="pj-ws">
|
|
257
|
+
<div class="pj-org-breadcrumb">
|
|
258
|
+
<button class="pj-org-crumb-link" onClick=${() => guardDirty(onBack)}>${t('organisms.title') || 'Organisms'}</button>
|
|
259
|
+
<span class="pj-org-crumb-sep">/</span>
|
|
260
|
+
${showSettings ? html`
|
|
261
|
+
<button class="pj-org-crumb-link" onClick=${() => guardDirty(() => setShowSettings(false))}>${(org.name || org.id)}</button>
|
|
262
|
+
<span class="pj-org-crumb-sep">/</span>
|
|
263
|
+
<span>${t('organisms.settings') || 'Settings'}</span>
|
|
264
|
+
` : html`<span>${(org.name || org.id)}</span>`}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div class="pj-org-home-head">
|
|
268
|
+
<div class="pj-org-avatar pj-org-avatar-lg" aria-hidden="true">${orgInitials(org.name)}</div>
|
|
269
|
+
<div class="pj-org-home-title">
|
|
270
|
+
<div class="pj-org-titlerow">
|
|
271
|
+
<span class="pj-org-home-name">${(org.name || org.id)}</span>
|
|
272
|
+
<span class="badge badge-info">${typeLabel}</span>
|
|
273
|
+
<span class="badge ${org.visibility === 'public' ? 'badge-success' : 'badge-warn'}">${org.visibility || ''}</span>
|
|
274
|
+
</div>
|
|
275
|
+
${org.description ? html`<div class="section-desc pj-org-home-desc">${(org.description)}</div>` : null}
|
|
276
|
+
</div>
|
|
277
|
+
<div class="pj-org-home-actions">
|
|
278
|
+
<button class="btn-outline btn-sm" title=${t('organisms.exportOrgHint') || 'Download a ZIP backup of the whole organism (all workspaces)'}
|
|
279
|
+
onClick=${() => exportOrganismZip(org, showToast)}>${'⬇ '}${t('organisms.exportOrg') || 'Export'}</button>
|
|
280
|
+
<button class="btn-outline btn-sm ${showSettings ? 'pj-org-btn-active' : ''}" onClick=${() => guardDirty(() => setShowSettings(s => !s))}>${'⚙ '}${t('organisms.settings') || 'Settings'}</button>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<${StructureOverview} label=${t('organisms.structureOverviewOrg') || 'Organism structure overview'}
|
|
285
|
+
load=${() => orgService.getOrganismOverview(org.id)} />
|
|
286
|
+
|
|
287
|
+
<div class="pj-org-tabs" role="tablist">
|
|
288
|
+
${tabs.map(tb => html`
|
|
289
|
+
<button class="pj-org-tab ${activeHomeTab === tb.id ? 'active' : ''}" role="tab" aria-selected=${activeHomeTab === tb.id} key=${tb.id}
|
|
290
|
+
onClick=${() => pickHomeTab(tb.id)}>
|
|
291
|
+
${tb.label}${tb.count !== null && tb.count !== undefined ? html`<span class="pj-org-tab-count">${tb.count}</span>` : null}
|
|
292
|
+
${tb.pill ? html`<span class="pj-tab-pill">${tb.pill}</span>` : null}
|
|
293
|
+
</button>`)}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
${showSettings ? renderSettings() : null}
|
|
297
|
+
|
|
298
|
+
${activeHomeTab === 'workspaces' ? html`
|
|
299
|
+
<${WorkspaceList} org=${org} showToast=${showToast} onOpen=${onOpenWs} onCount=${setWsCount} />
|
|
300
|
+
` : null}
|
|
301
|
+
|
|
302
|
+
${activeHomeTab === 'members' ? html`
|
|
303
|
+
<${OrgMemberManager} org=${org} ghii=${ghii} canManage=${canEdit} isCreator=${isCreator}
|
|
304
|
+
showToast=${showToast} confirm=${confirm} onChanged=${onChanged} show="members" />` : null}
|
|
305
|
+
|
|
306
|
+
${activeHomeTab === 'agents' ? html`
|
|
307
|
+
<${OrgAgentsPanel} org=${org} ghii=${ghii} canManage=${canEdit} showToast=${showToast} onChanged=${onChanged} />` : null}
|
|
308
|
+
|
|
309
|
+
${activeHomeTab === 'board' ? (org.boardId
|
|
310
|
+
? html`<${BoardPreview} boardId=${org.boardId} showToast=${showToast} />`
|
|
311
|
+
: html`<div class="card-detail"><div class="section-desc">${t('organisms.noBoard') || 'This organism has no board.'}</div></div>`) : null}
|
|
312
|
+
|
|
313
|
+
<${ConfirmUI} />
|
|
314
|
+
</div>`;
|
|
315
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file members.js
|
|
3
|
+
* @description Organism Members tab — roster first (avatar rows with role badge, per-workspace
|
|
4
|
+
* access line, joined date, "…" menu for make-creator/remove/block), "+ Invite" toggle, and
|
|
5
|
+
* pending join requests when present. For a regular member it is a read-only roster. Extracted
|
|
6
|
+
* from organisms-tab.js, no behaviour change.
|
|
7
|
+
* @structure OrgMemberManager
|
|
8
|
+
* @usage import { OrgMemberManager } from '/views/profile/organisms/members.js';
|
|
9
|
+
* @version-history
|
|
10
|
+
* v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
|
|
11
|
+
*/
|
|
12
|
+
import { h } from 'preact';
|
|
13
|
+
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import { KebabMenu } from '/views/profile/shared.js';
|
|
18
|
+
import { PresenceDot } from '/components/PresenceDot.js';
|
|
19
|
+
import * as orgService from '/js/services/organisms.js';
|
|
20
|
+
import { fmtDate, orgInitials, relTime } from '/views/profile/organisms/helpers.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Organism member panel. For creator/admin (`canManage`) it is a full manager: approve/reject
|
|
24
|
+
* join requests, invite by name, remove/block members, lift bans, transfer ownership (creator
|
|
25
|
+
* only), and attach/detach agents. For a regular member it renders a read-only roster + agent
|
|
26
|
+
* list. Backend: /:id/{join-requests,invitations,members,transfer,agents}. Refreshes the
|
|
27
|
+
* parent list via onChanged so member counts stay current.
|
|
28
|
+
*/
|
|
29
|
+
export function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, confirm, onChanged, show }) {
|
|
30
|
+
const orgId = org.id;
|
|
31
|
+
// Membership is keyed by bare owner name; presence needs a full GHII. Local members
|
|
32
|
+
// resolve via this node; an already-qualified (federated) ghii is passed through.
|
|
33
|
+
const myNode = (window.AIMEAT?.auth?.getSession?.()?.ghii || '').split('@')[1] || '';
|
|
34
|
+
const toGhii = (id) => (id && !id.includes('@')) ? (myNode ? `${id}@${myNode}` : '') : (id || '');
|
|
35
|
+
const [requests, setRequests] = useState(null);
|
|
36
|
+
const [members, setMembers] = useState(null);
|
|
37
|
+
const [banned, setBanned] = useState([]);
|
|
38
|
+
const [invitations, setInvitations] = useState([]);
|
|
39
|
+
const [inviteName, setInviteName] = useState('');
|
|
40
|
+
const [showInvite, setShowInvite] = useState(false);
|
|
41
|
+
const [busy, setBusy] = useState(false);
|
|
42
|
+
const [wsAccess, setWsAccess] = useState(null); // bare owner name → [{ ws, role }] (best effort)
|
|
43
|
+
|
|
44
|
+
const load = useCallback(async () => {
|
|
45
|
+
const tasks = [orgService.listMembers(orgId).catch(() => null)];
|
|
46
|
+
if (canManage) {
|
|
47
|
+
tasks.push(
|
|
48
|
+
orgService.listJoinRequests(orgId).catch(() => null),
|
|
49
|
+
orgService.listMembers(orgId, 'banned').catch(() => null),
|
|
50
|
+
orgService.listInvitations(orgId).catch(() => null),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const [mb, rq, bn, inv] = await Promise.all(tasks);
|
|
54
|
+
setMembers(mb?.data?.members || []);
|
|
55
|
+
if (canManage) {
|
|
56
|
+
setRequests(rq?.data?.join_requests || []);
|
|
57
|
+
setBanned(bn?.data?.members || []);
|
|
58
|
+
setInvitations(inv?.data?.invitations || []);
|
|
59
|
+
}
|
|
60
|
+
}, [orgId, canManage]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => { load(); }, [load]);
|
|
63
|
+
const liveRef = useRef(load); liveRef.current = load;
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const handler = () => liveRef.current();
|
|
66
|
+
window.addEventListener('aimeat-live-update', handler);
|
|
67
|
+
return () => window.removeEventListener('aimeat-live-update', handler);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
// Per-member workspace access (best effort): workspace creators from discovery + access lists of
|
|
71
|
+
// the workspaces the viewer created. Keyed by bare owner name (memberships use bare names too).
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (show !== 'members') return undefined;
|
|
74
|
+
let cancelled = false;
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
const wss = await orgService.discoverWorkspaces(orgId);
|
|
78
|
+
const map = {};
|
|
79
|
+
const add = (who, wsName, role) => {
|
|
80
|
+
const bare = String(who || '').split('@')[0];
|
|
81
|
+
if (!bare) return;
|
|
82
|
+
const list = map[bare] || (map[bare] = []);
|
|
83
|
+
if (!list.some(x => x.ws === wsName)) list.push({ ws: wsName, role });
|
|
84
|
+
};
|
|
85
|
+
for (const w of wss) if (w.created_by) add(w.created_by, w.name || w.id, 'owner');
|
|
86
|
+
await Promise.all(wss.filter(w => w.access === 'owner').map(async (w) => {
|
|
87
|
+
const acc = await orgService.getWorkspaceAccess(orgId, w.id).catch(() => null);
|
|
88
|
+
for (const m of (acc?.members || [])) add(m.owner, w.name || w.id, m.role);
|
|
89
|
+
}));
|
|
90
|
+
if (!cancelled) setWsAccess(map);
|
|
91
|
+
} catch { /* the access line is optional */ }
|
|
92
|
+
})();
|
|
93
|
+
return () => { cancelled = true; };
|
|
94
|
+
}, [orgId, show]);
|
|
95
|
+
|
|
96
|
+
const run = async (fn, okMsg, failKey) => {
|
|
97
|
+
setBusy(true);
|
|
98
|
+
try {
|
|
99
|
+
const r = await fn();
|
|
100
|
+
if (r?.ok === false) showToast(r?.error?.message || (t(failKey) || 'Failed'));
|
|
101
|
+
else if (okMsg) showToast(okMsg);
|
|
102
|
+
await load(); onChanged?.();
|
|
103
|
+
} catch (e) { showToast((e && e.message) || (t(failKey) || 'Failed')); }
|
|
104
|
+
finally { setBusy(false); }
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const review = (rid, decision) => run(
|
|
108
|
+
() => orgService.reviewJoinRequest(orgId, rid, decision),
|
|
109
|
+
decision === 'approved' ? (t('organisms.joinApproved') || 'Request approved') : (t('organisms.joinRejected') || 'Request declined'),
|
|
110
|
+
'organisms.reviewFailed');
|
|
111
|
+
|
|
112
|
+
const invite = () => {
|
|
113
|
+
const name = inviteName.trim();
|
|
114
|
+
if (!name) return;
|
|
115
|
+
setInviteName('');
|
|
116
|
+
run(() => orgService.inviteMember(orgId, name), (t('organisms.invitationSent') || 'Invitation sent'), 'organisms.inviteFailed');
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const remove = (memberGhii, ban) => confirm(
|
|
120
|
+
(ban ? (t('organisms.confirmBlockMember') || 'Block {member} and remove them from this organism?') : (t('organisms.confirmRemoveMember') || 'Revoke {member}’s access to this organism?')).replace('{member}', memberGhii),
|
|
121
|
+
() => run(() => orgService.removeMember(orgId, memberGhii, ban), ban ? (t('organisms.memberBlocked') || 'Member blocked') : (t('organisms.memberRemoved') || 'Member removed'), 'organisms.removeFailed'),
|
|
122
|
+
{ danger: true });
|
|
123
|
+
|
|
124
|
+
const unban = (memberGhii) => run(() => orgService.unbanMember(orgId, memberGhii), (t('organisms.banLifted') || 'Block lifted'), 'organisms.removeFailed');
|
|
125
|
+
|
|
126
|
+
const transfer = (toWhom) => confirm(
|
|
127
|
+
(t('organisms.confirmTransfer') || 'Make {member} the creator? You will become an admin.').replace('{member}', toWhom),
|
|
128
|
+
() => run(() => orgService.transferOwnership(orgId, toWhom), (t('organisms.ownershipTransferred') || 'Ownership transferred'), 'organisms.transferFailed'),
|
|
129
|
+
{ danger: true });
|
|
130
|
+
|
|
131
|
+
const pending = (requests || []).filter(r => r.status === 'pending');
|
|
132
|
+
const showMembers = show !== 'agents';
|
|
133
|
+
|
|
134
|
+
// "Access: Marketing (contributor)" line — creator shows "all workspaces" (mirrors reality:
|
|
135
|
+
// the organism creator governs every workspace it owns; per-ws data may be partial for others).
|
|
136
|
+
const accessLine = (m) => {
|
|
137
|
+
if (m.role === 'creator') return t('organisms.accessAll') || 'all workspaces';
|
|
138
|
+
const list = wsAccess?.[String(m.ghii || '').split('@')[0]] || [];
|
|
139
|
+
if (!list.length) return '';
|
|
140
|
+
return list.map(x => `${x.ws} (${x.role})`).join(', ');
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return html`
|
|
144
|
+
<div class="card-detail">
|
|
145
|
+
${showMembers ? html`
|
|
146
|
+
<div class="pj-tabhead">
|
|
147
|
+
<div class="section-desc pj-tabhead-desc">${t('organisms.membersDesc') || 'Members can join workspaces; their agents inherit the role.'}
|
|
148
|
+
${' '}<span class="pj-members-cap">${(members || []).length}/${org.maxMembers || 500}</span></div>
|
|
149
|
+
${canManage ? html`<button class="btn-primary btn-sm" onClick=${() => setShowInvite(s => !s)}>${'+ '}${t('organisms.invite') || 'Invite'}</button>` : null}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
${canManage && showInvite ? html`
|
|
153
|
+
<div class="flex-row-wrap pj-invite-row">
|
|
154
|
+
<input class="input-field input-sm" autofocus placeholder=${t('organisms.inviteePlaceholder') || 'owner name'} value=${inviteName}
|
|
155
|
+
onInput=${(e) => setInviteName(e.target.value)} onKeyDown=${(e) => { if (e.key === 'Enter') invite(); }} />
|
|
156
|
+
<button class="btn-outline btn-sm" disabled=${busy || !inviteName.trim()} onClick=${invite}>${t('organisms.invite') || 'Invite'}</button>
|
|
157
|
+
<button class="btn-ghost btn-sm" onClick=${() => setShowInvite(false)}>${t('organisms.cancel') || 'Cancel'}</button>
|
|
158
|
+
</div>` : null}
|
|
159
|
+
${canManage && invitations.length > 0 ? html`
|
|
160
|
+
<div class="section-desc">${t('organisms.outstandingInvites') || 'Awaiting acceptance'}: ${invitations.map(m => (m.ghii)).join(', ')}</div>
|
|
161
|
+
` : null}
|
|
162
|
+
|
|
163
|
+
${canManage && pending.length > 0 ? pending.map(r => html`
|
|
164
|
+
<div class="pj-org-row pj-req-row" key=${r.id}>
|
|
165
|
+
<div class="pj-org-avatar" aria-hidden="true">${'🙋'}</div>
|
|
166
|
+
<div class="pj-org-main pj-org-main-static">
|
|
167
|
+
<div class="pj-org-titlerow">
|
|
168
|
+
<span class="pj-org-name">${(r.ghii)}</span>
|
|
169
|
+
<span class="pj-org-desc">${t('organisms.wantsToJoin') || 'wants to join'}${r.createdAt ? ` · ${relTime(r.createdAt)}` : ''}</span>
|
|
170
|
+
</div>
|
|
171
|
+
${r.message ? html`<div class="pj-org-desc">${(r.message)}</div>` : null}
|
|
172
|
+
</div>
|
|
173
|
+
<button class="btn-ghost btn-sm" disabled=${busy} onClick=${() => review(r.id, 'rejected')}>${t('organisms.decline') || 'Decline'}</button>
|
|
174
|
+
<button class="btn-success btn-sm" disabled=${busy} onClick=${() => review(r.id, 'approved')}>${t('organisms.approve') || 'Approve'}</button>
|
|
175
|
+
</div>
|
|
176
|
+
`) : null}
|
|
177
|
+
|
|
178
|
+
<div class="pj-org-list">
|
|
179
|
+
${(members || []).map(m => {
|
|
180
|
+
const acc = accessLine(m);
|
|
181
|
+
const menuItems = (canManage && m.role !== 'creator' && m.ghii !== ghii) ? [
|
|
182
|
+
isCreator && { label: t('organisms.makeCreator') || 'Make creator', icon: '👑', onClick: () => transfer(m.ghii) },
|
|
183
|
+
{ label: t('organisms.remove') || 'Remove', danger: true, onClick: () => remove(m.ghii, false) },
|
|
184
|
+
{ label: t('organisms.block') || 'Block', danger: true, onClick: () => remove(m.ghii, true) },
|
|
185
|
+
] : [];
|
|
186
|
+
return html`
|
|
187
|
+
<div class="pj-org-row" key=${m.ghii}>
|
|
188
|
+
<div class="pj-org-avatar" aria-hidden="true">${orgInitials(m.ghii)}</div>
|
|
189
|
+
<div class="pj-org-main pj-org-main-static">
|
|
190
|
+
<div class="pj-org-titlerow">
|
|
191
|
+
<span class="pj-org-name">${(m.ghii)} <${PresenceDot} ghii=${toGhii(m.ghii)} /></span>
|
|
192
|
+
<span class="badge ${m.role === 'creator' ? 'badge-success' : 'badge-info'}">${(m.role || 'member')}</span>
|
|
193
|
+
</div>
|
|
194
|
+
${(acc || m.joinedAt) ? html`
|
|
195
|
+
<div class="pj-org-desc">
|
|
196
|
+
${acc ? `${t('organisms.accessLabel') || 'Access'}: ${acc}` : ''}${acc && m.joinedAt ? ' · ' : ''}${m.joinedAt ? (t('organisms.joinedDate') || 'joined {date}').replace('{date}', fmtDate(m.joinedAt)) : ''}
|
|
197
|
+
</div>` : null}
|
|
198
|
+
${(m.agents || []).length ? html`
|
|
199
|
+
<div class="pj-org-desc" title=${t('organisms.memberAgentsHint') || "This member's agents — they inherit the membership and can act in this organism"}>
|
|
200
|
+
${'🤖 '}${t('organisms.memberAgents') || 'Agents'}: ${m.agents.map(a => a.name || a.gaii).join(', ')}
|
|
201
|
+
</div>` : null}
|
|
202
|
+
</div>
|
|
203
|
+
${menuItems.length ? html`<${KebabMenu} label=${t('organisms.moreActions') || 'More actions'} items=${menuItems} />` : null}
|
|
204
|
+
</div>`;
|
|
205
|
+
})}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
${canManage && banned.length > 0 ? html`
|
|
209
|
+
<div class="detail-label">${t('organisms.blockedMembers') || 'Blocked'}</div>
|
|
210
|
+
${banned.map(m => html`
|
|
211
|
+
<div class="pj-access-row" key=${'ban-' + m.ghii}>
|
|
212
|
+
<span>${(m.ghii)}</span>
|
|
213
|
+
<button class="btn-ghost btn-sm" disabled=${busy} onClick=${() => unban(m.ghii)}>${t('organisms.unblock') || 'Unblock'}</button>
|
|
214
|
+
</div>
|
|
215
|
+
`)}
|
|
216
|
+
` : null}
|
|
217
|
+
` : null}
|
|
218
|
+
</div>
|
|
219
|
+
`;
|
|
220
|
+
}
|