cohvu 2.16.1 → 2.18.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/api.d.ts +55 -13
- package/dist/api.js +23 -8
- package/dist/api.js.map +1 -1
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -1
- package/dist/index.js +59 -19
- package/dist/index.js.map +1 -1
- package/dist/postinstall.js +1 -1
- package/dist/postinstall.js.map +1 -1
- package/dist/teardown.js +26 -0
- package/dist/teardown.js.map +1 -1
- package/dist/tui/App.js +197 -89
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/LockCard.d.ts +17 -0
- package/dist/tui/components/LockCard.js +37 -0
- package/dist/tui/components/LockCard.js.map +1 -0
- package/dist/tui/components/Modal.js +5 -2
- package/dist/tui/components/Modal.js.map +1 -1
- package/dist/tui/state.d.ts +24 -2
- package/dist/tui/state.js +45 -0
- package/dist/tui/state.js.map +1 -1
- package/dist/tui/tabs/KeysTab.js +29 -4
- package/dist/tui/tabs/KeysTab.js.map +1 -1
- package/dist/tui/tabs/KnowledgeTab.js +20 -0
- package/dist/tui/tabs/KnowledgeTab.js.map +1 -1
- package/dist/tui/tabs/ProjectTab.js +4 -1
- package/dist/tui/tabs/ProjectTab.js.map +1 -1
- package/dist/tui/tabs/TeamTab.js +10 -3
- package/dist/tui/tabs/TeamTab.js.map +1 -1
- package/dist/tui/tabs/YouTab.js +2 -2
- package/dist/tui/tabs/YouTab.js.map +1 -1
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Direct translation of dashboard.ts into React hooks.
|
|
4
4
|
import { useReducer, useRef, useEffect, useCallback } from 'react';
|
|
5
5
|
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
6
|
-
import { reduce, initialState, TABS, getActiveProject, getActiveTeam } from './state.js';
|
|
6
|
+
import { reduce, initialState, TABS, getActiveProject, getActiveTeam, isTeamLocked, filterToSameEntity } from './state.js';
|
|
7
7
|
import { ApiClient } from '../api.js';
|
|
8
8
|
import { runSetup } from '../setup.js';
|
|
9
9
|
import { detectPlatformStatuses } from './platform-detect.js';
|
|
@@ -76,12 +76,6 @@ export default function App() {
|
|
|
76
76
|
execFile(cmd, [url], () => { });
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
function shouldRequireConsensus(s) {
|
|
80
|
-
if (!s.requireConsensus)
|
|
81
|
-
return false;
|
|
82
|
-
const admins = s.members.filter(m => m.role === 'admin');
|
|
83
|
-
return admins.length >= 2;
|
|
84
|
-
}
|
|
85
79
|
function copyToClipboard(text) {
|
|
86
80
|
const cmd = process.platform === 'darwin' ? 'pbcopy'
|
|
87
81
|
: process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
|
|
@@ -278,6 +272,23 @@ export default function App() {
|
|
|
278
272
|
}
|
|
279
273
|
catch { }
|
|
280
274
|
}, 30_000);
|
|
275
|
+
// Team overview powers the lock card when a team sub is canceled. It's
|
|
276
|
+
// cheap and always reflects the current team's shape — fetch on every
|
|
277
|
+
// project switch into a team project, clear when switching to personal.
|
|
278
|
+
const project = stateRef.current.projects.find(p => p.project_id === projectId);
|
|
279
|
+
if (project?.owner.kind === 'team') {
|
|
280
|
+
const teamId = project.owner.teamId;
|
|
281
|
+
api.getTeamOverview(teamId)
|
|
282
|
+
.then(overview => {
|
|
283
|
+
if (stateRef.current.activeProjectId === projectId) {
|
|
284
|
+
dispatch({ type: 'SET_TEAM_OVERVIEW', overview });
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
.catch(() => { });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
dispatch({ type: 'SET_TEAM_OVERVIEW', overview: null });
|
|
291
|
+
}
|
|
281
292
|
setTimeout(() => loadTabData(), 0);
|
|
282
293
|
}, [api, connectFeed, dispatch, loadTabData]);
|
|
283
294
|
const executeSearch = useCallback(async () => {
|
|
@@ -482,6 +493,43 @@ export default function App() {
|
|
|
482
493
|
await handleModalKey(input, key);
|
|
483
494
|
return;
|
|
484
495
|
}
|
|
496
|
+
// Team sub canceled — admins can hit `r` from any team tab to start
|
|
497
|
+
// resubscribe checkout. Members just see the contact list in the lock
|
|
498
|
+
// card; `r` does nothing for them.
|
|
499
|
+
if (isTeamLocked(s) && input === 'r' && s.userRole === 'admin') {
|
|
500
|
+
const team = getActiveTeam(s);
|
|
501
|
+
if (team) {
|
|
502
|
+
try {
|
|
503
|
+
showToast('Opening checkout...', 'info');
|
|
504
|
+
const c = await api.createTeamCheckout(team.team_id);
|
|
505
|
+
if (c.checkout_url)
|
|
506
|
+
openBrowser(c.checkout_url);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
showToast('Failed to open checkout', 'error');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Pro canceled with locked personal project in view — `u` opens Pro
|
|
515
|
+
// checkout. One checkout unlocks every personal project at once, no
|
|
516
|
+
// per-project choice. Integration keys stay disabled and need manual
|
|
517
|
+
// restore after.
|
|
518
|
+
if (input === 'u' && !s.modal) {
|
|
519
|
+
const project = getActiveProject(s);
|
|
520
|
+
if (project?.owner.kind === 'personal' && project.locked) {
|
|
521
|
+
try {
|
|
522
|
+
showToast('Opening checkout...', 'info');
|
|
523
|
+
const c = await api.createIndividualCheckout();
|
|
524
|
+
if (c.checkout_url)
|
|
525
|
+
openBrowser(c.checkout_url);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
showToast('Failed to open checkout', 'error');
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
485
533
|
// Approval keys (when approvals exist, on team/project tab, team project)
|
|
486
534
|
if ((input === 'a' || input === 'x') && !s.modal && s.pendingApprovals.length > 0 && (s.tab === 'team' || s.tab === 'project')) {
|
|
487
535
|
const activeProject = getActiveProject(s);
|
|
@@ -492,7 +540,7 @@ export default function App() {
|
|
|
492
540
|
kind: 'approve-action',
|
|
493
541
|
approvalId: approval.id,
|
|
494
542
|
description: approval.description,
|
|
495
|
-
initiator: approval.initiator_email,
|
|
543
|
+
initiator: approval.initiator_email ?? approval.initiator_name ?? 'unknown',
|
|
496
544
|
expiresIn: timeUntil(approval.expires_at),
|
|
497
545
|
} });
|
|
498
546
|
}
|
|
@@ -593,6 +641,37 @@ export default function App() {
|
|
|
593
641
|
}
|
|
594
642
|
return;
|
|
595
643
|
}
|
|
644
|
+
// Restore a disabled integration key. Only valid when the selected key
|
|
645
|
+
// is disabled — otherwise `r` is a no-op here. Server rotates the
|
|
646
|
+
// secret and returns it once; TUI shows the "new secret" modal so the
|
|
647
|
+
// admin can copy it into their external systems.
|
|
648
|
+
if (input === 'r' && s.userRole === 'admin' && s.apiKeys.length > 0) {
|
|
649
|
+
const target = s.apiKeys[s.apiKeysSelected];
|
|
650
|
+
if (target && target.disabled_at) {
|
|
651
|
+
try {
|
|
652
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Restoring key' });
|
|
653
|
+
const { api_key, key } = await api.restoreApiKey(target.id);
|
|
654
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
655
|
+
dispatch({ type: 'OPEN_MODAL', modal: {
|
|
656
|
+
kind: 'key-created',
|
|
657
|
+
keyId: api_key.id,
|
|
658
|
+
keyValue: key,
|
|
659
|
+
keyName: api_key.name,
|
|
660
|
+
kind2: 'integration',
|
|
661
|
+
} });
|
|
662
|
+
// Refresh the list so the restored key moves out of the disabled section.
|
|
663
|
+
if (s.activeProjectId) {
|
|
664
|
+
const keys = await api.listIntegrationKeys(s.activeProjectId);
|
|
665
|
+
dispatch({ type: 'SET_API_KEYS', keys });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
670
|
+
showToast('Failed to restore key', 'error');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
596
675
|
}
|
|
597
676
|
// ---- Knowledge keys ----
|
|
598
677
|
async function handleKnowledgeKey(input, key) {
|
|
@@ -747,10 +826,22 @@ export default function App() {
|
|
|
747
826
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-rename-team', input: '' } });
|
|
748
827
|
return;
|
|
749
828
|
}
|
|
750
|
-
// 'r' key on invite link row — regen
|
|
829
|
+
// 'r' key on invite link row — regen (member/viewer) or mint-new (admin).
|
|
830
|
+
// Admin regen is semantically the same as mint since each admin link is
|
|
831
|
+
// single-use; we just mint and surface instead of asking for confirmation.
|
|
751
832
|
if (input === 'r' && s.userRole === 'admin' && onLinkRow) {
|
|
752
833
|
const linkIdx = sel - memberCount - 3;
|
|
753
834
|
const role = linkRoles[linkIdx];
|
|
835
|
+
if (role === 'admin' && team) {
|
|
836
|
+
try {
|
|
837
|
+
const link = await api.mintAdminInvite(team.team_id);
|
|
838
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite-link', role: 'admin', url: link.url } });
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
showToast('Failed to mint admin invite', 'error');
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
754
845
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-regen-link', role } });
|
|
755
846
|
return;
|
|
756
847
|
}
|
|
@@ -782,10 +873,13 @@ export default function App() {
|
|
|
782
873
|
}
|
|
783
874
|
return;
|
|
784
875
|
}
|
|
785
|
-
// 'c' key on invite link row — copy link
|
|
876
|
+
// 'c' key on invite link row — copy link. Admin row has no persistent
|
|
877
|
+
// URL (admin invites are minted on demand) so 'c' is a no-op there.
|
|
786
878
|
if (input === 'c' && s.userRole === 'admin' && onLinkRow) {
|
|
787
879
|
const linkIdx = sel - memberCount - 3;
|
|
788
880
|
const role = linkRoles[linkIdx];
|
|
881
|
+
if (role === 'admin')
|
|
882
|
+
return;
|
|
789
883
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
790
884
|
if (link) {
|
|
791
885
|
copyToClipboard(link.url);
|
|
@@ -796,15 +890,34 @@ export default function App() {
|
|
|
796
890
|
}
|
|
797
891
|
return;
|
|
798
892
|
}
|
|
799
|
-
// 'o' key on invite link row — open in browser
|
|
893
|
+
// 'o' key on invite link row — open in browser. Same as 'c', admin row
|
|
894
|
+
// has nothing to open since the link doesn't exist until minted.
|
|
800
895
|
if (input === 'o' && s.userRole === 'admin' && onLinkRow) {
|
|
801
896
|
const linkIdx = sel - memberCount - 3;
|
|
802
897
|
const role = linkRoles[linkIdx];
|
|
898
|
+
if (role === 'admin')
|
|
899
|
+
return;
|
|
803
900
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
804
901
|
if (link)
|
|
805
902
|
openBrowser(link.url);
|
|
806
903
|
return;
|
|
807
904
|
}
|
|
905
|
+
// Enter on the admin invite row — mint a fresh short-lived single-use
|
|
906
|
+
// link and surface it via the invite-link modal (copy/open affordances).
|
|
907
|
+
if (key.return && s.userRole === 'admin' && onLinkRow) {
|
|
908
|
+
const linkIdx = sel - memberCount - 3;
|
|
909
|
+
const role = linkRoles[linkIdx];
|
|
910
|
+
if (role === 'admin' && team) {
|
|
911
|
+
try {
|
|
912
|
+
const link = await api.mintAdminInvite(team.team_id);
|
|
913
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite-link', role: 'admin', url: link.url } });
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
showToast('Failed to mint admin invite', 'error');
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
808
921
|
// 'd' key (admin) — delete team
|
|
809
922
|
if (input === 'd' && s.userRole === 'admin') {
|
|
810
923
|
if (team) {
|
|
@@ -1033,7 +1146,10 @@ export default function App() {
|
|
|
1033
1146
|
}
|
|
1034
1147
|
// Integration key creation wizard
|
|
1035
1148
|
if (modal.kind === 'create-integration-key') {
|
|
1036
|
-
|
|
1149
|
+
// Silo rule: once one project is selected, only same-entity projects
|
|
1150
|
+
// are reachable. Filter here so index arithmetic in the picker stays
|
|
1151
|
+
// consistent with what the Modal renders.
|
|
1152
|
+
const adminProjects = filterToSameEntity(adminScopableProjects(s), modal.selectedProjectIds, s.user?.id ?? null);
|
|
1037
1153
|
if (modal.step === 'name') {
|
|
1038
1154
|
if (key.return && modal.input.trim().length > 0) {
|
|
1039
1155
|
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'agent' } });
|
|
@@ -1265,30 +1381,23 @@ export default function App() {
|
|
|
1265
1381
|
return;
|
|
1266
1382
|
}
|
|
1267
1383
|
}
|
|
1268
|
-
if (shouldRequireConsensus(stateRef.current) && modal.currentRole === 'admin' && newRole !== 'admin') {
|
|
1269
|
-
const team = getActiveTeam(stateRef.current);
|
|
1270
|
-
if (team) {
|
|
1271
|
-
try {
|
|
1272
|
-
await api.initiateApproval(team.team_id, 'demote_admin', `demote ${modal.targetEmail} from admin`, modal.targetUserId);
|
|
1273
|
-
const approvals = await api.listApprovals(team.team_id);
|
|
1274
|
-
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1275
|
-
dispatch({ type: 'CLOSE_MODAL' });
|
|
1276
|
-
showToast('Approval requested', 'info');
|
|
1277
|
-
}
|
|
1278
|
-
catch {
|
|
1279
|
-
showToast('Failed to request approval', 'error');
|
|
1280
|
-
dispatch({ type: 'CLOSE_MODAL' });
|
|
1281
|
-
}
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
1384
|
const changeTeam = getActiveTeam(s);
|
|
1286
1385
|
if (changeTeam) {
|
|
1287
1386
|
try {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1387
|
+
// Server decides whether consensus applies — if so, it returns
|
|
1388
|
+
// 202 with a pending_approval object; otherwise the role change
|
|
1389
|
+
// applies immediately. We don't branch client-side.
|
|
1390
|
+
const resp = await api.changeTeamRole(changeTeam.team_id, modal.targetUserId, newRole);
|
|
1391
|
+
if (resp.pending_approval) {
|
|
1392
|
+
const approvals = await api.listApprovals(changeTeam.team_id);
|
|
1393
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1394
|
+
showToast('Role change proposed — waiting for majority', 'info');
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
const members = await api.listTeamMembers(changeTeam.team_id);
|
|
1398
|
+
dispatch({ type: 'SET_MEMBERS', members });
|
|
1399
|
+
showToast('Role updated', 'success');
|
|
1400
|
+
}
|
|
1292
1401
|
}
|
|
1293
1402
|
catch {
|
|
1294
1403
|
showToast('Failed to update role', 'error');
|
|
@@ -1336,6 +1445,26 @@ export default function App() {
|
|
|
1336
1445
|
}
|
|
1337
1446
|
else if (key.return) {
|
|
1338
1447
|
const role = roles[modal.selected];
|
|
1448
|
+
const team = getActiveTeam(s);
|
|
1449
|
+
if (!team) {
|
|
1450
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1451
|
+
showToast('No team selected', 'error');
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (role === 'admin') {
|
|
1455
|
+
// Admin invites are minted on the fly — short-lived, single-use.
|
|
1456
|
+
// No persistent link in state.inviteLinks; each invite is fresh.
|
|
1457
|
+
try {
|
|
1458
|
+
const link = await api.mintAdminInvite(team.team_id);
|
|
1459
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1460
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite-link', role, url: link.url } });
|
|
1461
|
+
}
|
|
1462
|
+
catch {
|
|
1463
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1464
|
+
showToast('Failed to mint admin invite', 'error');
|
|
1465
|
+
}
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1339
1468
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
1340
1469
|
if (link) {
|
|
1341
1470
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
@@ -1529,16 +1658,19 @@ export default function App() {
|
|
|
1529
1658
|
return false;
|
|
1530
1659
|
const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1531
1660
|
try {
|
|
1532
|
-
dispatch({ type: 'SET_OPERATION', operation: '
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1661
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Opening team checkout' });
|
|
1662
|
+
// Team creation is gated on a team subscription — we hand the user
|
|
1663
|
+
// to Stripe checkout; the webhook creates the team + sub atomically
|
|
1664
|
+
// on payment. The new team appears on the next me() refresh.
|
|
1665
|
+
const { checkout_url } = await api.createTeam(modal.input, slug);
|
|
1666
|
+
if (checkout_url)
|
|
1667
|
+
openBrowser(checkout_url);
|
|
1536
1668
|
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1537
|
-
|
|
1669
|
+
showToast('Complete checkout in your browser — team will appear after payment', 'info');
|
|
1538
1670
|
}
|
|
1539
1671
|
catch {
|
|
1540
1672
|
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1541
|
-
showToast('Failed to
|
|
1673
|
+
showToast('Failed to start team checkout', 'error');
|
|
1542
1674
|
}
|
|
1543
1675
|
return false;
|
|
1544
1676
|
}
|
|
@@ -1659,27 +1791,25 @@ export default function App() {
|
|
|
1659
1791
|
case 'confirm-clear': {
|
|
1660
1792
|
const project = getActiveProject(s);
|
|
1661
1793
|
if (project && modal.input === project.slug) {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1794
|
+
try {
|
|
1795
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Clearing contributions' });
|
|
1796
|
+
// Server gates this behind consensus for team projects. If the
|
|
1797
|
+
// response includes pending_approval, the action is queued.
|
|
1798
|
+
const resp = await api.clearMemories(projectId);
|
|
1799
|
+
if (resp.pending_approval) {
|
|
1800
|
+
const team = getActiveTeam(s);
|
|
1801
|
+
if (team) {
|
|
1667
1802
|
const approvals = await api.listApprovals(team.team_id);
|
|
1668
1803
|
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1669
|
-
showToast('Approval requested', 'info');
|
|
1670
1804
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1805
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1806
|
+
showToast('Clear proposed — waiting for majority', 'info');
|
|
1807
|
+
}
|
|
1808
|
+
else {
|
|
1809
|
+
dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
|
|
1810
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1811
|
+
showToast('All contributions cleared', 'success');
|
|
1675
1812
|
}
|
|
1676
|
-
}
|
|
1677
|
-
try {
|
|
1678
|
-
dispatch({ type: 'SET_OPERATION', operation: 'Clearing contributions' });
|
|
1679
|
-
await api.clearMemories(projectId);
|
|
1680
|
-
dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
|
|
1681
|
-
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1682
|
-
showToast('All contributions cleared', 'success');
|
|
1683
1813
|
}
|
|
1684
1814
|
catch {
|
|
1685
1815
|
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
@@ -1691,21 +1821,6 @@ export default function App() {
|
|
|
1691
1821
|
case 'confirm-delete': {
|
|
1692
1822
|
const project = getActiveProject(s);
|
|
1693
1823
|
if (project && modal.input === project.slug) {
|
|
1694
|
-
if (shouldRequireConsensus(s)) {
|
|
1695
|
-
const team = getActiveTeam(s);
|
|
1696
|
-
if (team) {
|
|
1697
|
-
try {
|
|
1698
|
-
await api.initiateApproval(team.team_id, 'delete_project', `delete project "${project.slug}"`, projectId);
|
|
1699
|
-
const approvals = await api.listApprovals(team.team_id);
|
|
1700
|
-
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1701
|
-
showToast('Approval requested', 'info');
|
|
1702
|
-
}
|
|
1703
|
-
catch {
|
|
1704
|
-
showToast('Failed to request approval', 'error');
|
|
1705
|
-
}
|
|
1706
|
-
break;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
1824
|
try {
|
|
1710
1825
|
dispatch({ type: 'SET_OPERATION', operation: 'Deleting project' });
|
|
1711
1826
|
await api.deleteProject(projectId);
|
|
@@ -1848,24 +1963,16 @@ export default function App() {
|
|
|
1848
1963
|
case 'confirm-delete-team': {
|
|
1849
1964
|
const teamForDelete = getActiveTeam(s);
|
|
1850
1965
|
if (teamForDelete && modal.input === modal.slug) {
|
|
1851
|
-
if (shouldRequireConsensus(s)) {
|
|
1852
|
-
const team = getActiveTeam(s);
|
|
1853
|
-
if (team) {
|
|
1854
|
-
try {
|
|
1855
|
-
await api.initiateApproval(team.team_id, 'delete_team', `delete team "${team.name}"`);
|
|
1856
|
-
const approvals = await api.listApprovals(team.team_id);
|
|
1857
|
-
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1858
|
-
showToast('Approval requested', 'info');
|
|
1859
|
-
}
|
|
1860
|
-
catch {
|
|
1861
|
-
showToast('Failed to request approval', 'error');
|
|
1862
|
-
}
|
|
1863
|
-
break;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
1966
|
try {
|
|
1867
1967
|
dispatch({ type: 'SET_OPERATION', operation: 'Deleting team' });
|
|
1868
|
-
await api.deleteTeam(teamForDelete.team_id);
|
|
1968
|
+
const resp = await api.deleteTeam(teamForDelete.team_id);
|
|
1969
|
+
if (resp.pending_approval) {
|
|
1970
|
+
const approvals = await api.listApprovals(teamForDelete.team_id);
|
|
1971
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1972
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1973
|
+
showToast('Team deletion proposed — waiting for majority', 'info');
|
|
1974
|
+
break;
|
|
1975
|
+
}
|
|
1869
1976
|
const me = await api.me();
|
|
1870
1977
|
dispatch({ type: 'SET_USER_DATA', me });
|
|
1871
1978
|
dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
|
|
@@ -1996,11 +2103,12 @@ function hasBillingBanner(state) {
|
|
|
1996
2103
|
function deriveFlatProjectsFromMe(me) {
|
|
1997
2104
|
const list = [];
|
|
1998
2105
|
for (const p of me.personal_projects) {
|
|
1999
|
-
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, owner: { kind: 'personal' } });
|
|
2106
|
+
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, locked: p.locked ?? false, owner: { kind: 'personal' } });
|
|
2000
2107
|
}
|
|
2001
2108
|
for (const team of me.teams) {
|
|
2109
|
+
const teamCanceled = team.subscription?.status === 'canceled';
|
|
2002
2110
|
for (const p of team.projects) {
|
|
2003
|
-
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, owner: { kind: 'team', teamId: team.team_id, teamName: team.name, teamSlug: team.slug } });
|
|
2111
|
+
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, locked: teamCanceled, owner: { kind: 'team', teamId: team.team_id, teamName: team.name, teamSlug: team.slug } });
|
|
2004
2112
|
}
|
|
2005
2113
|
}
|
|
2006
2114
|
return list;
|