clay-server 2.28.0 → 2.29.0-beta.1
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/lib/daemon.js +14 -0
- package/lib/project-connection.js +2 -1
- package/lib/project-sessions.js +25 -0
- package/lib/public/app.js +1 -0
- package/lib/public/css/filebrowser.css +7 -0
- package/lib/public/css/overlays.css +27 -8
- package/lib/public/index.html +2 -0
- package/lib/public/modules/app-messages.js +1 -0
- package/lib/public/modules/app-projects.js +49 -22
- package/lib/public/modules/project-settings.js +9 -4
- package/lib/public/modules/sidebar-mobile.js +1 -0
- package/lib/public/modules/sidebar-projects.js +1 -1
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -267,6 +267,20 @@ var relay = createServer({
|
|
|
267
267
|
spawnOpts.gid = parseInt(passwdLine[1], 10);
|
|
268
268
|
} catch (e) {}
|
|
269
269
|
}
|
|
270
|
+
// Pre-create target directory and chown to user so git clone can write into it
|
|
271
|
+
if (config.osUsers && wsUser && wsUser.linuxUser && spawnOpts.uid != null) {
|
|
272
|
+
if (fs.existsSync(targetDir)) {
|
|
273
|
+
callback({ ok: false, error: "Target directory already exists: " + targetDir });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
fs.mkdirSync(targetDir, { mode: 0o700 });
|
|
278
|
+
fs.chownSync(targetDir, spawnOpts.uid, spawnOpts.gid);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
callback({ ok: false, error: "Failed to prepare project directory: " + e.message });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
270
284
|
var proc = spawn("git", ["clone", cloneUrl, targetDir], spawnOpts);
|
|
271
285
|
var stderrBuf = "";
|
|
272
286
|
proc.stderr.on("data", function (chunk) { stderrBuf += chunk.toString(); });
|
|
@@ -81,7 +81,8 @@ function attachConnection(ctx) {
|
|
|
81
81
|
var _filteredProjects = getProjectList(_userId);
|
|
82
82
|
var title = getTitle();
|
|
83
83
|
var project = getProject();
|
|
84
|
-
|
|
84
|
+
var ownerLocked = !!(osUsers && osUsers.length > 0 && /^\/home\/[^/]+\//.test(cwd));
|
|
85
|
+
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
|
|
85
86
|
var latestVersion = getLatestVersion();
|
|
86
87
|
if (latestVersion && ws._clayUser && ws._clayUser.role === "admin") {
|
|
87
88
|
sendTo(ws, { type: "update_available", version: latestVersion });
|
package/lib/project-sessions.js
CHANGED
|
@@ -112,6 +112,11 @@ function attachSessions(ctx) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (msg.type === "transfer_project_owner") {
|
|
115
|
+
// Home directory projects: ownership is permanently locked
|
|
116
|
+
if (osUsers && osUsers.length > 0 && /^\/home\/[^/]+\//.test(cwd)) {
|
|
117
|
+
sendTo(ws, { type: "error", text: "Cannot transfer ownership of home directory projects." });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
115
120
|
var projectOwnerId = getProjectOwnerId();
|
|
116
121
|
var isAdmin = ws._clayUser && ws._clayUser.role === "admin";
|
|
117
122
|
var isProjectOwner = ws._clayUser && projectOwnerId && ws._clayUser.id === projectOwnerId;
|
|
@@ -845,6 +850,14 @@ function attachSessions(ctx) {
|
|
|
845
850
|
if (msg.type === "browse_dir") {
|
|
846
851
|
var rawPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
|
|
847
852
|
var absTarget = path.resolve(rawPath);
|
|
853
|
+
// Multi-user mode: non-admins can only browse their home directory
|
|
854
|
+
if (osUsers && osUsers.length > 0 && ws._clayUser && ws._clayUser.role !== "admin") {
|
|
855
|
+
var browseHome = ws._clayUser.linuxUser ? "/home/" + ws._clayUser.linuxUser : null;
|
|
856
|
+
if (!browseHome || (absTarget !== browseHome && (absTarget + "/").indexOf(browseHome + "/") !== 0)) {
|
|
857
|
+
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: "Access restricted to your home directory" });
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
848
861
|
var parentDir, prefix;
|
|
849
862
|
try {
|
|
850
863
|
var stat = fs.statSync(absTarget);
|
|
@@ -884,6 +897,18 @@ function attachSessions(ctx) {
|
|
|
884
897
|
if (msg.type === "add_project") {
|
|
885
898
|
var addPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
|
|
886
899
|
var addAbs = path.resolve(addPath);
|
|
900
|
+
// Multi-user mode: normal users restricted to their home directory
|
|
901
|
+
if (osUsers && osUsers.length > 0 && ws._clayUser && ws._clayUser.role !== "admin") {
|
|
902
|
+
if (!ws._clayUser.linuxUser) {
|
|
903
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "No Linux user assigned" });
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
var userHome = "/home/" + ws._clayUser.linuxUser;
|
|
907
|
+
if (addAbs !== userHome && (addAbs + "/").indexOf(userHome + "/") !== 0) {
|
|
908
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Path not allowed. You can only add directories under " + userHome });
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
887
912
|
try {
|
|
888
913
|
var addStat = fs.statSync(addAbs);
|
|
889
914
|
if (!addStat.isDirectory()) {
|
package/lib/public/app.js
CHANGED
|
@@ -379,6 +379,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
379
379
|
get multiUser() { return store.getState().isMultiUserMode; },
|
|
380
380
|
get myUserId() { return store.getState().myUserId; },
|
|
381
381
|
get projectOwnerId() { return store.getState().currentProjectOwnerId; },
|
|
382
|
+
get ownerLocked() { return store.getState().ownerLocked; },
|
|
382
383
|
openDm: function (userId) { openDm(userId); },
|
|
383
384
|
openMateWizard: function () { requireClayMateInterview(function () { openMateWizard(); }); },
|
|
384
385
|
openAddProjectModal: function () { openAddProjectModal(); },
|
|
@@ -478,7 +478,14 @@
|
|
|
478
478
|
display: flex;
|
|
479
479
|
align-items: center;
|
|
480
480
|
gap: 8px;
|
|
481
|
+
flex-wrap: wrap;
|
|
481
482
|
}
|
|
483
|
+
.ps-owner-locked-hint {
|
|
484
|
+
font-size: 12px;
|
|
485
|
+
color: var(--text-muted);
|
|
486
|
+
width: 100%;
|
|
487
|
+
}
|
|
488
|
+
.ps-owner-locked-hint.hidden { display: none; }
|
|
482
489
|
|
|
483
490
|
.ps-transfer-form {
|
|
484
491
|
margin-top: 8px;
|
|
@@ -1012,22 +1012,41 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
|
|
|
1012
1012
|
|
|
1013
1013
|
.add-project-body { margin-bottom: 16px; }
|
|
1014
1014
|
|
|
1015
|
-
.add-project-input-wrap { position: relative; }
|
|
1015
|
+
.add-project-input-wrap { position: relative; display: flex; align-items: center; background: var(--input-bg); border: 1px solid var(--border); border-radius: 8px; transition: border-color 0.2s; }
|
|
1016
|
+
.add-project-input-wrap:focus-within { border-color: var(--text-dimmer); }
|
|
1017
|
+
|
|
1018
|
+
.add-project-prefix {
|
|
1019
|
+
flex-shrink: 0;
|
|
1020
|
+
padding: 5px 0 5px 8px;
|
|
1021
|
+
font-size: 12px;
|
|
1022
|
+
font-family: "Roboto Mono", monospace;
|
|
1023
|
+
color: var(--accent);
|
|
1024
|
+
background: var(--bg-alt);
|
|
1025
|
+
border-radius: 5px 0 0 5px;
|
|
1026
|
+
white-space: nowrap;
|
|
1027
|
+
user-select: none;
|
|
1028
|
+
pointer-events: none;
|
|
1029
|
+
margin: 4px 0 4px 4px;
|
|
1030
|
+
padding: 3px 6px;
|
|
1031
|
+
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
|
1032
|
+
border-radius: 4px;
|
|
1033
|
+
}
|
|
1034
|
+
.add-project-prefix.hidden { display: none; }
|
|
1016
1035
|
|
|
1017
1036
|
#add-project-input {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
border
|
|
1037
|
+
flex: 1;
|
|
1038
|
+
min-width: 0;
|
|
1039
|
+
background: transparent;
|
|
1040
|
+
border: none;
|
|
1022
1041
|
color: var(--text);
|
|
1023
1042
|
font-size: 13px;
|
|
1024
1043
|
font-family: "Roboto Mono", monospace;
|
|
1025
|
-
padding: 10px 12px;
|
|
1044
|
+
padding: 10px 12px 10px 6px;
|
|
1026
1045
|
outline: none;
|
|
1027
|
-
transition: border-color 0.2s;
|
|
1028
1046
|
}
|
|
1047
|
+
.add-project-prefix.hidden + #add-project-input { padding-left: 12px; }
|
|
1029
1048
|
|
|
1030
|
-
#add-project-input:focus { border-color:
|
|
1049
|
+
#add-project-input:focus { border-color: transparent; }
|
|
1031
1050
|
#add-project-input::placeholder { color: var(--text-muted); font-family: "Pretendard", system-ui, sans-serif; }
|
|
1032
1051
|
|
|
1033
1052
|
#add-project-suggestions {
|
package/lib/public/index.html
CHANGED
|
@@ -641,6 +641,7 @@
|
|
|
641
641
|
<div class="ps-owner-row">
|
|
642
642
|
<span class="settings-value" id="ps-owner-name">-</span>
|
|
643
643
|
<button class="settings-btn-sm" id="ps-transfer-btn">Transfer</button>
|
|
644
|
+
<span class="ps-owner-locked-hint hidden" id="ps-owner-locked-hint">Ownership of home directory projects cannot be transferred.</span>
|
|
644
645
|
</div>
|
|
645
646
|
<div class="ps-transfer-form hidden" id="ps-transfer-form">
|
|
646
647
|
<select class="ps-select" id="ps-transfer-select"></select>
|
|
@@ -1461,6 +1462,7 @@
|
|
|
1461
1462
|
<div class="add-project-body">
|
|
1462
1463
|
<div class="add-project-panel active" data-panel="existing">
|
|
1463
1464
|
<div class="add-project-input-wrap">
|
|
1465
|
+
<span id="add-project-prefix" class="add-project-prefix hidden"></span>
|
|
1464
1466
|
<input type="text" id="add-project-input" placeholder="/" autocomplete="off" spellcheck="false">
|
|
1465
1467
|
<div id="add-project-suggestions" class="hidden"></div>
|
|
1466
1468
|
</div>
|
|
@@ -263,6 +263,7 @@ export function processMessage(msg) {
|
|
|
263
263
|
if (serverVersionEl) serverVersionEl.textContent = msg.version;
|
|
264
264
|
}
|
|
265
265
|
if (msg.projectOwnerId !== undefined) store.setState({ currentProjectOwnerId: msg.projectOwnerId });
|
|
266
|
+
if (msg.ownerLocked !== undefined) store.setState({ ownerLocked: !!msg.ownerLocked });
|
|
266
267
|
if (msg.osUsers !== undefined) store.setState({ isOsUsers: !!msg.osUsers });
|
|
267
268
|
if (msg.lanHost) window.__lanHost = msg.lanHost;
|
|
268
269
|
if (msg.dangerouslySkipPermissions) {
|
|
@@ -27,6 +27,8 @@ var addProjectCancel = null;
|
|
|
27
27
|
var addProjectModeBtns = null;
|
|
28
28
|
var addProjectPanels = null;
|
|
29
29
|
var addProjectRemoved = null;
|
|
30
|
+
var addProjectPrefixEl = null;
|
|
31
|
+
var addProjectPrefixValue = "";
|
|
30
32
|
var addProjectDebounce = null;
|
|
31
33
|
var addProjectActiveIdx = -1;
|
|
32
34
|
var addProjectMode = "existing";
|
|
@@ -47,6 +49,7 @@ export function initProjects(ctx) {
|
|
|
47
49
|
addProjectModeBtns = addProjectModal.querySelectorAll(".add-project-mode-btn");
|
|
48
50
|
addProjectPanels = addProjectModal.querySelectorAll(".add-project-panel");
|
|
49
51
|
addProjectRemoved = document.getElementById("add-project-removed");
|
|
52
|
+
addProjectPrefixEl = document.getElementById("add-project-prefix");
|
|
50
53
|
|
|
51
54
|
// Mode button click listeners
|
|
52
55
|
for (var mbi = 0; mbi < addProjectModeBtns.length; mbi++) {
|
|
@@ -59,7 +62,7 @@ export function initProjects(ctx) {
|
|
|
59
62
|
// Existing project input listeners
|
|
60
63
|
addProjectInput.addEventListener("focus", function () {
|
|
61
64
|
var val = addProjectInput.value;
|
|
62
|
-
if (val && addProjectSuggestions.children.length === 0) {
|
|
65
|
+
if ((val || addProjectPrefixValue) && addProjectSuggestions.children.length === 0) {
|
|
63
66
|
requestBrowseDir(val);
|
|
64
67
|
} else if (addProjectSuggestions.children.length > 0) {
|
|
65
68
|
addProjectSuggestions.classList.remove("hidden");
|
|
@@ -109,10 +112,10 @@ export function initProjects(ctx) {
|
|
|
109
112
|
? items[addProjectActiveIdx]
|
|
110
113
|
: items.length > 0 ? items[0] : null;
|
|
111
114
|
if (target) {
|
|
112
|
-
var
|
|
113
|
-
addProjectInput.value =
|
|
115
|
+
var fullP = target.dataset.path + "/";
|
|
116
|
+
addProjectInput.value = stripPrefix(fullP);
|
|
114
117
|
addProjectError.classList.add("hidden");
|
|
115
|
-
requestBrowseDir(
|
|
118
|
+
requestBrowseDir(addProjectInput.value);
|
|
116
119
|
}
|
|
117
120
|
return;
|
|
118
121
|
}
|
|
@@ -120,10 +123,10 @@ export function initProjects(ctx) {
|
|
|
120
123
|
if (e.key === "Enter") {
|
|
121
124
|
e.preventDefault();
|
|
122
125
|
if (addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length) {
|
|
123
|
-
var
|
|
124
|
-
addProjectInput.value =
|
|
126
|
+
var fullPicked = items[addProjectActiveIdx].dataset.path + "/";
|
|
127
|
+
addProjectInput.value = stripPrefix(fullPicked);
|
|
125
128
|
addProjectError.classList.add("hidden");
|
|
126
|
-
requestBrowseDir(
|
|
129
|
+
requestBrowseDir(addProjectInput.value);
|
|
127
130
|
return;
|
|
128
131
|
}
|
|
129
132
|
submitAddProject();
|
|
@@ -614,7 +617,6 @@ function switchAddProjectMode(mode) {
|
|
|
614
617
|
|
|
615
618
|
export function openAddProjectModal() {
|
|
616
619
|
addProjectModal.classList.remove("hidden");
|
|
617
|
-
addProjectInput.value = "/";
|
|
618
620
|
addProjectCreateInput.value = "";
|
|
619
621
|
addProjectCloneInput.value = "";
|
|
620
622
|
addProjectError.classList.add("hidden");
|
|
@@ -626,17 +628,29 @@ export function openAddProjectModal() {
|
|
|
626
628
|
addProjectOk.disabled = false;
|
|
627
629
|
var existingBtn = addProjectModal.querySelector('.add-project-mode-btn[data-mode="existing"]');
|
|
628
630
|
if (_ctx.isOsUsers) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
631
|
+
existingBtn.disabled = false;
|
|
632
|
+
var myUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
|
|
633
|
+
var isAdmin = myUser && myUser.role === "admin";
|
|
634
|
+
if (!isAdmin && myUser && myUser.linuxUser) {
|
|
635
|
+
// Non-admin: lock prefix to home directory
|
|
636
|
+
addProjectPrefixValue = "/home/" + myUser.linuxUser + "/";
|
|
637
|
+
addProjectPrefixEl.textContent = addProjectPrefixValue;
|
|
638
|
+
addProjectPrefixEl.classList.remove("hidden");
|
|
639
|
+
addProjectInput.value = "";
|
|
640
|
+
addProjectInput.placeholder = "subdirectory";
|
|
641
|
+
} else {
|
|
642
|
+
// Admin: no prefix restriction
|
|
643
|
+
addProjectPrefixValue = "";
|
|
644
|
+
addProjectPrefixEl.classList.add("hidden");
|
|
645
|
+
addProjectInput.value = "/";
|
|
646
|
+
addProjectInput.placeholder = "/";
|
|
638
647
|
}
|
|
648
|
+
switchAddProjectMode("existing");
|
|
639
649
|
} else {
|
|
650
|
+
addProjectPrefixValue = "";
|
|
651
|
+
addProjectPrefixEl.classList.add("hidden");
|
|
652
|
+
addProjectInput.value = "/";
|
|
653
|
+
addProjectInput.placeholder = "/";
|
|
640
654
|
existingBtn.disabled = false;
|
|
641
655
|
switchAddProjectMode("existing");
|
|
642
656
|
}
|
|
@@ -693,12 +707,25 @@ export function closeAddProjectModal() {
|
|
|
693
707
|
addProjectError.classList.add("hidden");
|
|
694
708
|
addProjectCloneProgress.classList.add("hidden");
|
|
695
709
|
addProjectActiveIdx = -1;
|
|
710
|
+
addProjectPrefixValue = "";
|
|
711
|
+
addProjectPrefixEl.classList.add("hidden");
|
|
696
712
|
if (addProjectDebounce) { clearTimeout(addProjectDebounce); addProjectDebounce = null; }
|
|
697
713
|
}
|
|
698
714
|
|
|
715
|
+
function getFullPath(inputVal) {
|
|
716
|
+
return addProjectPrefixValue + inputVal;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function stripPrefix(fullPath) {
|
|
720
|
+
if (addProjectPrefixValue && fullPath.indexOf(addProjectPrefixValue) === 0) {
|
|
721
|
+
return fullPath.slice(addProjectPrefixValue.length);
|
|
722
|
+
}
|
|
723
|
+
return fullPath;
|
|
724
|
+
}
|
|
725
|
+
|
|
699
726
|
function requestBrowseDir(val) {
|
|
700
727
|
if (!_ctx.getWs() || _ctx.getWs().readyState !== 1) return;
|
|
701
|
-
_ctx.getWs().send(JSON.stringify({ type: "browse_dir", path: val }));
|
|
728
|
+
_ctx.getWs().send(JSON.stringify({ type: "browse_dir", path: getFullPath(val) }));
|
|
702
729
|
}
|
|
703
730
|
|
|
704
731
|
export function handleBrowseDirResult(msg) {
|
|
@@ -721,11 +748,11 @@ export function handleBrowseDirResult(msg) {
|
|
|
721
748
|
item.innerHTML = '<i data-lucide="folder"></i><span class="add-project-suggestion-name">' +
|
|
722
749
|
escapeHtml(entry.name) + '</span>';
|
|
723
750
|
item.addEventListener("click", function (e) {
|
|
724
|
-
var
|
|
725
|
-
addProjectInput.value =
|
|
751
|
+
var fullP = this.dataset.path + "/";
|
|
752
|
+
addProjectInput.value = stripPrefix(fullP);
|
|
726
753
|
addProjectInput.focus();
|
|
727
754
|
addProjectError.classList.add("hidden");
|
|
728
|
-
requestBrowseDir(
|
|
755
|
+
requestBrowseDir(addProjectInput.value);
|
|
729
756
|
});
|
|
730
757
|
addProjectSuggestions.appendChild(item);
|
|
731
758
|
}
|
|
@@ -777,7 +804,7 @@ function submitAddProject() {
|
|
|
777
804
|
addProjectOk.disabled = true;
|
|
778
805
|
|
|
779
806
|
if (addProjectMode === "existing") {
|
|
780
|
-
var val = addProjectInput.value.replace(/\/+$/, "");
|
|
807
|
+
var val = getFullPath(addProjectInput.value).replace(/\/+$/, "");
|
|
781
808
|
if (!val) { addProjectOk.disabled = false; return; }
|
|
782
809
|
if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
|
|
783
810
|
_ctx.getWs().send(JSON.stringify({ type: "add_project", path: val }));
|
|
@@ -250,12 +250,17 @@ function populateProfile() {
|
|
|
250
250
|
var ownerField = document.getElementById("ps-owner-field");
|
|
251
251
|
if (ownerField) {
|
|
252
252
|
var ownerId = currentProject ? currentProject.projectOwnerId : null;
|
|
253
|
+
var isOwnerLocked = currentProject ? currentProject.ownerLocked : false;
|
|
253
254
|
var isMultiUser = ctx.multiUser;
|
|
254
255
|
if (isMultiUser) {
|
|
255
256
|
ownerField.style.display = "";
|
|
256
257
|
var ownerNameEl = document.getElementById("ps-owner-name");
|
|
257
258
|
var transferBtn = document.getElementById("ps-transfer-btn");
|
|
259
|
+
var ownerLockedHint = document.getElementById("ps-owner-locked-hint");
|
|
258
260
|
if (transferBtn) transferBtn.style.display = "none";
|
|
261
|
+
if (ownerLockedHint) {
|
|
262
|
+
if (isOwnerLocked) { ownerLockedHint.classList.remove("hidden"); } else { ownerLockedHint.classList.add("hidden"); }
|
|
263
|
+
}
|
|
259
264
|
// Fetch user list (only succeeds for admin)
|
|
260
265
|
fetch("/api/admin/users").then(function (r) {
|
|
261
266
|
if (!r.ok) throw new Error("not admin");
|
|
@@ -272,14 +277,14 @@ function populateProfile() {
|
|
|
272
277
|
} else {
|
|
273
278
|
if (ownerNameEl) ownerNameEl.textContent = "Not set";
|
|
274
279
|
}
|
|
275
|
-
// Admin can
|
|
276
|
-
if (transferBtn) transferBtn.style.display = "";
|
|
280
|
+
// Admin can transfer unless ownership is locked (home directory projects)
|
|
281
|
+
if (transferBtn && !isOwnerLocked) transferBtn.style.display = "";
|
|
277
282
|
}).catch(function () {
|
|
278
283
|
// Not admin, show owner name from limited info
|
|
279
284
|
if (ownerId) {
|
|
280
285
|
if (ownerNameEl) ownerNameEl.textContent = ownerId;
|
|
281
|
-
// Project owner can also transfer
|
|
282
|
-
if (ctx.myUserId && ctx.myUserId === ownerId && transferBtn) {
|
|
286
|
+
// Project owner can also transfer unless locked
|
|
287
|
+
if (!isOwnerLocked && ctx.myUserId && ctx.myUserId === ownerId && transferBtn) {
|
|
283
288
|
transferBtn.style.display = "";
|
|
284
289
|
}
|
|
285
290
|
} else {
|
|
@@ -1055,6 +1055,7 @@ function renderSheetSettings(listEl) {
|
|
|
1055
1055
|
}
|
|
1056
1056
|
}
|
|
1057
1057
|
}
|
|
1058
|
+
if (proj && _ctx.ownerLocked) proj = Object.assign({}, proj, { ownerLocked: true });
|
|
1058
1059
|
openProjectSettings(getCachedCurrentSlug(), proj);
|
|
1059
1060
|
}, 250);
|
|
1060
1061
|
} else if (item.action === "server-settings") {
|
|
@@ -578,7 +578,7 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
578
578
|
settingsItem.addEventListener("click", function (e) {
|
|
579
579
|
e.stopPropagation();
|
|
580
580
|
closeProjectCtxMenu();
|
|
581
|
-
openProjectSettings(slug, { slug: slug, name: name, icon: icon, projectOwnerId: _ctx.projectOwnerId });
|
|
581
|
+
openProjectSettings(slug, { slug: slug, name: name, icon: icon, projectOwnerId: _ctx.projectOwnerId, ownerLocked: _ctx.ownerLocked });
|
|
582
582
|
});
|
|
583
583
|
menu.appendChild(settingsItem);
|
|
584
584
|
}
|