quar 1.1.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/LICENSE +0 -0
- package/README.md +93 -0
- package/index.js +64 -0
- package/package.json +23 -0
- package/public/assets/icon.png +0 -0
- package/public/scripts/insertTab.js +103 -0
- package/public/scripts/keyboardCommands.js +16 -0
- package/public/scripts/main.js +327 -0
- package/public/scripts/popup.js +26 -0
- package/public/styles/insertTab.css +57 -0
- package/public/styles/popup.css +71 -0
- package/public/styles/style.css +341 -0
- package/public/styles/variables.css +9 -0
- package/server.js +239 -0
- package/utils/loadModels.js +25 -0
- package/views/base.zare +16 -0
- package/views/components/insertTab.zare +8 -0
- package/views/components/popup.zare +12 -0
- package/views/pages/index.zare +55 -0
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Quar Studio – Quick UI for Mongoose Models
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./public/assets/icon.png" alt="logo" width="200px"/>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Instantly spin up a web-based editor for your Mongoose models.
|
|
8
|
+
Built for developers who loves quick database interaction!
|
|
9
|
+
|
|
10
|
+
## Install & Run
|
|
11
|
+
```bash
|
|
12
|
+
npx quar --model <path-to-model-folder> --db <db-name>
|
|
13
|
+
```
|
|
14
|
+
This will load all Mongoose models from the folder and start a local web UI to Create, view, update, and delete documents.
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
- Loads your Mongoose models dynamically
|
|
18
|
+
- Connects to your MongoDB database
|
|
19
|
+
- Gives a tabbed UI for each model
|
|
20
|
+
- View nested documents in a tree-style
|
|
21
|
+
- Supports Create, Read, update and delete
|
|
22
|
+
|
|
23
|
+
## Folder Structure
|
|
24
|
+
Your models folder can contain files like this:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// ./models/Post.js
|
|
28
|
+
import mongoose from "mongoose"
|
|
29
|
+
|
|
30
|
+
const postSchema = new mongoose.Schema({
|
|
31
|
+
title: String,
|
|
32
|
+
body: String,
|
|
33
|
+
author: {
|
|
34
|
+
name: String,
|
|
35
|
+
age: Number
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export default mongoose.model("Post", postSchema);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
1. Install Quar as dev deps
|
|
45
|
+
```bash
|
|
46
|
+
npm i quar --save-dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. Execute Command
|
|
50
|
+
```bash
|
|
51
|
+
npx quar
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. Set Models Folder
|
|
55
|
+
```bash
|
|
56
|
+
npx quar --model <folder-path>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
4. Set Database Name
|
|
60
|
+
```bash
|
|
61
|
+
npx quar --model <folder-path> --db <db-name>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
5. Set Database URI `(Optional)`
|
|
65
|
+
```bash
|
|
66
|
+
npx quar --model <folder-path> --db <db-name> --uri <db-uri>
|
|
67
|
+
```
|
|
68
|
+
> ***Note:** By default it will try to connect local mongodb if uri not provided
|
|
69
|
+
|
|
70
|
+
## Usage Tips
|
|
71
|
+
- Supports nested schemas & subdocuments
|
|
72
|
+
- Clean tabbed navigation per model
|
|
73
|
+
- Auto-refreshes data after updates
|
|
74
|
+
- Keyboard shortcuts support
|
|
75
|
+
|
|
76
|
+
| Command | Performs |
|
|
77
|
+
|---------|----------|
|
|
78
|
+
| <kbd>ctrl + o</kbd> | Toggles Insert Tab |
|
|
79
|
+
| <kbd>ctrl + r</kbd> | Refresh Model |
|
|
80
|
+
| <kbd>ctrl + ></kbd> | Go To Next Page |
|
|
81
|
+
| <kbd>ctrl + <</kbd> | Go To Previous Page |
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Dev Notes
|
|
86
|
+
|
|
87
|
+
- Built for development use
|
|
88
|
+
- Doesn’t expose MONGO_URI publicly
|
|
89
|
+
- Uses Mongoose’s model.schema.tree for schema info
|
|
90
|
+
- Modular and extendable
|
|
91
|
+
|
|
92
|
+
## Contribute
|
|
93
|
+
Pull requests, suggestions, and ideas are welcome!
|
package/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import minimist from 'minimist';
|
|
5
|
+
import app from './server.js';
|
|
6
|
+
import loadModels from './utils/loadModels.js';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import mongoose from 'mongoose';
|
|
10
|
+
|
|
11
|
+
const args = minimist(process.argv.slice(2));
|
|
12
|
+
|
|
13
|
+
if (!args.model) {
|
|
14
|
+
console.log(
|
|
15
|
+
chalk.red.bold('[ERROR]') + ' Missing required flag: ' + chalk.yellow('--model')
|
|
16
|
+
);
|
|
17
|
+
console.log('Usage: ' + chalk.cyan('quar --model <model-folder> --db <database-name>'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof args.model !== 'string') {
|
|
22
|
+
console.log(
|
|
23
|
+
chalk.red.bold('[ERROR]') + ' The value for ' + chalk.yellow('--model') + ' must be a string.'
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!args.db) {
|
|
29
|
+
console.log(
|
|
30
|
+
chalk.red.bold('[ERROR]') + ' Missing required flag: ' + chalk.yellow('--db')
|
|
31
|
+
);
|
|
32
|
+
console.log('Usage: ' + chalk.cyan('quar --model <model-folder> --db <database-name>'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof args.db !== 'string') {
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.red.bold('[ERROR]') + ' The value for ' + chalk.yellow('--db') + ' must be a string.'
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const modelDir = path.resolve(process.cwd(), args.model);
|
|
44
|
+
const fullModelPath = path.resolve(modelDir);
|
|
45
|
+
|
|
46
|
+
// 🔃 Load Models and Start Server
|
|
47
|
+
try {
|
|
48
|
+
loadModels(fullModelPath);
|
|
49
|
+
app.locals.modelPath = fullModelPath;
|
|
50
|
+
|
|
51
|
+
(async () => {
|
|
52
|
+
|
|
53
|
+
await mongoose.connect(`${args.uri || "mongodb://localhost:27017/"}${args.db}`)
|
|
54
|
+
app.listen(app.get('port'), () => {
|
|
55
|
+
const url = `http://127.0.0.1:${app.get('port')}`;
|
|
56
|
+
console.log(chalk.green.bold('[SUCCESS]') + ` Server is running on ${chalk.underline(url)}`);
|
|
57
|
+
open(url);
|
|
58
|
+
});
|
|
59
|
+
})();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.log(chalk.red.bold('[FATAL ERROR]') + ' Failed to launch the server.');
|
|
62
|
+
console.error(chalk.red(err.stack));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "quar",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "This will load all Mongoose models from the folder and start a local web UI to Create, view, update, and delete documents.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"quar": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js"
|
|
12
|
+
},
|
|
13
|
+
"author": "@IsmailBinMujeeb (IsmailBinMujeeb@gmail.com)",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"chalk": "^5.4.1",
|
|
17
|
+
"express": "^5.1.0",
|
|
18
|
+
"minimist": "^1.2.8",
|
|
19
|
+
"mongoose": "^8.14.1",
|
|
20
|
+
"open": "^10.1.2",
|
|
21
|
+
"zare": "^2.1.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
async function toggleInsertTab() {
|
|
2
|
+
const modelName = content.dataset?.modelName;
|
|
3
|
+
if (!modelName) {
|
|
4
|
+
showModal('error', 'Error Occurred!', 'No model selected.');
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const insertTab = document.querySelector('.insert-tab');
|
|
9
|
+
insertTab.classList.toggle('active');
|
|
10
|
+
|
|
11
|
+
if (insertTab.innerHTML.trim() !== '') return;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`/schema/${modelName}`);
|
|
15
|
+
if (!res.ok) throw new Error();
|
|
16
|
+
|
|
17
|
+
const schema = await res.json();
|
|
18
|
+
|
|
19
|
+
const form = document.createElement('form');
|
|
20
|
+
form.id = 'insert-form';
|
|
21
|
+
|
|
22
|
+
form.addEventListener('submit', async (e) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
|
|
25
|
+
const formData = new FormData(e.target);
|
|
26
|
+
const data = Object.fromEntries(formData.entries());
|
|
27
|
+
|
|
28
|
+
const res = await fetch(`/insert/${modelName}`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify(data)
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const error = await res.json();
|
|
36
|
+
showModal('error', 'Error Occurred!', error.error || 'Something went wrong, please try again.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
loadDocuments();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
for (const key in schema) {
|
|
44
|
+
const field = schema[key];
|
|
45
|
+
|
|
46
|
+
const label = document.createElement('label');
|
|
47
|
+
label.innerHTML = `${key}:`;
|
|
48
|
+
label.className = 'label';
|
|
49
|
+
label.htmlFor = key;
|
|
50
|
+
|
|
51
|
+
if (field.type === 'ObjectId') {
|
|
52
|
+
const idRes = await fetch(`/id/${field.ref}`);
|
|
53
|
+
if (!idRes.ok) {
|
|
54
|
+
const error = await idRes.json();
|
|
55
|
+
showModal('error', 'Error Occurred!', error.error || 'Something went wrong, please try again.');
|
|
56
|
+
return;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ids = await idRes.json();
|
|
60
|
+
const select = document.createElement('select');
|
|
61
|
+
select.id = key;
|
|
62
|
+
select.name = key;
|
|
63
|
+
select.className = 'input';
|
|
64
|
+
|
|
65
|
+
ids.forEach(id => {
|
|
66
|
+
const option = document.createElement('option');
|
|
67
|
+
option.value = id;
|
|
68
|
+
option.innerText = id;
|
|
69
|
+
select.appendChild(option);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
form.append(label, select);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const input = document.createElement('input');
|
|
77
|
+
input.type = field.type || 'text';
|
|
78
|
+
input.placeholder = field.type;
|
|
79
|
+
input.id = key;
|
|
80
|
+
input.name = key;
|
|
81
|
+
input.className = 'input';
|
|
82
|
+
input.required = isRequired(field.require);
|
|
83
|
+
if (field.default !== undefined) input.value = field.default;
|
|
84
|
+
|
|
85
|
+
form.append(label, input);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const submitBtn = document.createElement('button');
|
|
89
|
+
submitBtn.type = 'submit';
|
|
90
|
+
submitBtn.innerHTML = 'Submit';
|
|
91
|
+
submitBtn.className = 'btn';
|
|
92
|
+
form.appendChild(submitBtn);
|
|
93
|
+
|
|
94
|
+
insertTab.appendChild(form);
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
showModal('error', 'Error Occurred!', error.message || 'Something went wrong, please try again.');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isRequired(require) {
|
|
102
|
+
return Array.isArray(require) ? require[0] : require;
|
|
103
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
document.addEventListener("keydown", (e) => {
|
|
2
|
+
|
|
3
|
+
if (e.ctrlKey && e.key === 'o') {
|
|
4
|
+
e.preventDefault();
|
|
5
|
+
toggleInsertTab();
|
|
6
|
+
} else if (e.ctrlKey && e.key == "r") {
|
|
7
|
+
e.preventDefault()
|
|
8
|
+
loadDocuments();
|
|
9
|
+
} else if (e.ctrlKey && e.key == ".") {
|
|
10
|
+
e.preventDefault()
|
|
11
|
+
gotoNextPage();
|
|
12
|
+
} else if (e.ctrlKey && e.key == ",") {
|
|
13
|
+
e.preventDefault()
|
|
14
|
+
gotoPreviousPage();
|
|
15
|
+
}
|
|
16
|
+
})
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const tabsContainer = document.getElementById("tabs");
|
|
2
|
+
const content = document.getElementById("content");
|
|
3
|
+
let currentModelName = "";
|
|
4
|
+
let loadedDocuments = [];
|
|
5
|
+
const openTabs = {};
|
|
6
|
+
|
|
7
|
+
function openModel(modelName) {
|
|
8
|
+
|
|
9
|
+
if (!openTabs[modelName]) {
|
|
10
|
+
// Create a new tab
|
|
11
|
+
const tab = document.createElement("div");
|
|
12
|
+
tab.className = "tab";
|
|
13
|
+
tab.id = "tab-" + modelName;
|
|
14
|
+
tab.draggable = true;
|
|
15
|
+
tab.innerHTML = modelName + '<span class="close" onclick="closeTab(event, \'' + modelName + '\')">×</span>';
|
|
16
|
+
tab.onclick = () => activateTab(modelName);
|
|
17
|
+
tabsContainer.appendChild(tab);
|
|
18
|
+
|
|
19
|
+
tab.addEventListener("dragstart", dragStart);
|
|
20
|
+
tab.addEventListener("dragover", dragOver);
|
|
21
|
+
tab.addEventListener("drop", drop);
|
|
22
|
+
tab.addEventListener("dragend", dragEnd);
|
|
23
|
+
|
|
24
|
+
openTabs[modelName] = tab;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
activateTab(modelName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function activateTab(modelName) {
|
|
32
|
+
|
|
33
|
+
// Prevent if tab is already active
|
|
34
|
+
if (content.dataset?.modelName == modelName) return
|
|
35
|
+
|
|
36
|
+
content.dataset.modelName = modelName;
|
|
37
|
+
document.getElementById("page-value").innerText = 1;
|
|
38
|
+
|
|
39
|
+
const insertTab = document.querySelector('.insert-tab');
|
|
40
|
+
insertTab.classList.remove('active');
|
|
41
|
+
insertTab.innerHTML = "";
|
|
42
|
+
// Remove active class from all tabs
|
|
43
|
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
44
|
+
|
|
45
|
+
// Set current tab active
|
|
46
|
+
const currentTab = document.getElementById("tab-" + modelName);
|
|
47
|
+
currentTab.classList.add("active");
|
|
48
|
+
|
|
49
|
+
// Update content
|
|
50
|
+
await loadDocuments();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function closeTab(event, modelName) {
|
|
54
|
+
event.stopPropagation(); // prevent tab click
|
|
55
|
+
const tab = document.getElementById("tab-" + modelName);
|
|
56
|
+
tab.remove();
|
|
57
|
+
delete openTabs[modelName];
|
|
58
|
+
|
|
59
|
+
// Clear content if that tab was active and check if other tabs are open to be active
|
|
60
|
+
if (tab.classList.contains("active")) {
|
|
61
|
+
content.innerHTML = "";
|
|
62
|
+
const keys = Object.keys(openTabs);
|
|
63
|
+
const modelName = keys[keys.length - 1] || null;
|
|
64
|
+
if (modelName) activateTab(modelName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function loadDocuments() {
|
|
69
|
+
|
|
70
|
+
content.innerHTML = "";
|
|
71
|
+
const limit = document.getElementById("limit").value;
|
|
72
|
+
const page = document.getElementById("page-value").innerText;
|
|
73
|
+
const modelName = content.dataset?.modelName;
|
|
74
|
+
|
|
75
|
+
if (!modelName) {
|
|
76
|
+
showModal('error', 'Error Occurred!', 'No model selected.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const response = await fetch(`/models/${modelName}?limit=${limit}&page=${page}`);
|
|
81
|
+
if (response.status !== 200) {
|
|
82
|
+
showModal('error', 'Error Occurred!', 'Something went wrong, please try again.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
|
|
87
|
+
document.getElementById("document-count").innerText = data.count;
|
|
88
|
+
document.getElementById("page-value").dataset.totalPages = data.totalPages;
|
|
89
|
+
document.getElementById(`${modelName}-doc-count`).innerText = data.totalCount;
|
|
90
|
+
|
|
91
|
+
if (Number(page) >= Number(data.totalPages)) document.getElementById("next-page").disabled = true;
|
|
92
|
+
else document.getElementById("next-page").disabled = false;
|
|
93
|
+
|
|
94
|
+
if (Number(page) <= 1) document.getElementById("previous-page").disabled = true;
|
|
95
|
+
else document.getElementById("previous-page").disabled = false;
|
|
96
|
+
|
|
97
|
+
currentModelName = modelName;
|
|
98
|
+
loadedDocuments = data.documents;
|
|
99
|
+
// Render each array element as its own tree
|
|
100
|
+
data.documents.forEach((doc, index) => {
|
|
101
|
+
const wrapper = document.createElement('div');
|
|
102
|
+
wrapper.appendChild(createTree(doc, data.schema, index));
|
|
103
|
+
wrapper.innerHTML += `<div class="document-actions"><button class="updateDocument" onclick="updateDocument(${index})">Update</button><button class="deleteDocument" onclick="deleteDoc('${modelName}', '${doc._id}')">Delete</button></div>`;
|
|
104
|
+
content.innerHTML += wrapper.innerHTML;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function gotoNextPage() {
|
|
109
|
+
const page = document.getElementById("page-value");
|
|
110
|
+
page.innerText = Number(page.innerText) + 1;
|
|
111
|
+
|
|
112
|
+
if (Number(page.innerText) > Number(page.dataset.totalPages)) {
|
|
113
|
+
page.innerText = Number(page.dataset.totalPages);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
loadDocuments();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function gotoPreviousPage() {
|
|
121
|
+
const page = document.getElementById("page-value").innerText;
|
|
122
|
+
document.getElementById("page-value").innerText = Number(page) - 1;
|
|
123
|
+
|
|
124
|
+
if (Number(page) - 1 < 1) {
|
|
125
|
+
document.getElementById("page-value").innerText = 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
loadDocuments();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createTree(obj, schema, index, level = "root") {
|
|
133
|
+
|
|
134
|
+
const moduleDocument = document.createElement('div');
|
|
135
|
+
moduleDocument.classList.add('document');
|
|
136
|
+
const container = document.createElement('ul');
|
|
137
|
+
container.classList.add('tree');
|
|
138
|
+
|
|
139
|
+
for (let key in obj) {
|
|
140
|
+
const value = obj[key];
|
|
141
|
+
const type = schema[key]?.instance;
|
|
142
|
+
const li = document.createElement('li');
|
|
143
|
+
|
|
144
|
+
if (typeof value === 'object' && value !== null) {
|
|
145
|
+
li.innerHTML = `<span class="caret collapsible">${key} ${type || "Object"}</span>`;
|
|
146
|
+
const child = createTree(value, schema[key]?.options?.type?.paths, index, level + "." + key);
|
|
147
|
+
child.classList.add('nested');
|
|
148
|
+
child.classList.remove('document');
|
|
149
|
+
li.appendChild(child);
|
|
150
|
+
} else {
|
|
151
|
+
li.innerHTML = `<span class="key">${key}</span> : <span class="value ${type}" ${type === "ObjectId" ? "contenteditable='false'" : "contenteditable='true'"} data-level="${level}" data-index="${index}">${value}</span>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
container.appendChild(li);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
moduleDocument.appendChild(container);
|
|
158
|
+
return moduleDocument;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle collapsibles
|
|
162
|
+
document.addEventListener('click', function (e) {
|
|
163
|
+
if (e.target.classList.contains('collapsible')) {
|
|
164
|
+
e.target.classList.toggle("caret-down");
|
|
165
|
+
const nested = e.target.nextElementSibling;
|
|
166
|
+
if (nested) {
|
|
167
|
+
nested.classList.toggle("active");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
function setNestedValue(obj, path, value) {
|
|
173
|
+
const keys = path.split(".");
|
|
174
|
+
let current = obj;
|
|
175
|
+
|
|
176
|
+
keys.forEach((key, index) => {
|
|
177
|
+
if (index === keys.length - 1) {
|
|
178
|
+
current[key] = value; // Set value at last key
|
|
179
|
+
} else {
|
|
180
|
+
if (!current[key] || typeof current[key] !== "object") {
|
|
181
|
+
current[key] = {}; // Create nested object if it doesn't exist
|
|
182
|
+
}
|
|
183
|
+
current = current[key]; // Go deeper
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return obj; // Optional: return the updated object
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function updateDocument(index) {
|
|
191
|
+
|
|
192
|
+
const confirmUpdate = confirm("Are you sure you want to update this document?");
|
|
193
|
+
if (!confirmUpdate) return;
|
|
194
|
+
|
|
195
|
+
const doc = loadedDocuments[index];
|
|
196
|
+
const id = doc._id;
|
|
197
|
+
|
|
198
|
+
let updatedDoc = {};
|
|
199
|
+
const values = document.querySelectorAll(`[data-index="${index}"]`);
|
|
200
|
+
|
|
201
|
+
values.forEach(el => {
|
|
202
|
+
const key = el.previousElementSibling?.innerText?.trim();
|
|
203
|
+
const level = el.dataset.level;
|
|
204
|
+
if (key) {
|
|
205
|
+
if (level && level !== "root") {
|
|
206
|
+
// Build the full path
|
|
207
|
+
const fullPath = `${level.replace("root.", "")}.${key}`;
|
|
208
|
+
setNestedValue(updatedDoc, fullPath, el.innerText);
|
|
209
|
+
} else {
|
|
210
|
+
updatedDoc[key] = el.innerText;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const res = await fetch(`/update/${currentModelName}/${id}`, {
|
|
216
|
+
method: "PUT",
|
|
217
|
+
headers: { "Content-Type": "application/json" },
|
|
218
|
+
body: JSON.stringify(updatedDoc),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (res.ok) {
|
|
222
|
+
showModal('info', 'Document Updated', 'Document updated successfully.');
|
|
223
|
+
loadDocuments();
|
|
224
|
+
} else {
|
|
225
|
+
const data = await res.json();
|
|
226
|
+
showModal('error', 'Update Failed', data.error || 'Update failed, please try again.');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function deleteDoc(modelName, id) {
|
|
231
|
+
const confirmDelete = confirm("Are you sure you want to delete this document?");
|
|
232
|
+
if (!confirmDelete) return;
|
|
233
|
+
|
|
234
|
+
const res = await fetch(`/delete/${modelName}/${id}`, {
|
|
235
|
+
method: "DELETE"
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (res.ok) {
|
|
239
|
+
showModal('info', 'Document Deleted', 'Document deleted successfully.');
|
|
240
|
+
openModel(modelName);
|
|
241
|
+
const data = await res.json();
|
|
242
|
+
document.getElementById(`${modelName}-doc-count`).innerText = data.count || 0;
|
|
243
|
+
} else {
|
|
244
|
+
showModal('error', 'Delete Failed', await res.json().error || 'Delete failed, please try again.');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function deleteAllDocs() {
|
|
249
|
+
try {
|
|
250
|
+
const confirmDelete = confirm("Are you sure you want to delete all documents?");
|
|
251
|
+
if (!confirmDelete) return;
|
|
252
|
+
|
|
253
|
+
const modelName = content.dataset?.modelName;
|
|
254
|
+
if (!modelName) {
|
|
255
|
+
showModal('error', 'Error Occurred!', 'No model selected.');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const res = await fetch(`/delete-all/${modelName}`, { method: "DELETE" });
|
|
260
|
+
if (res.ok) {
|
|
261
|
+
showModal('info', 'All Documents Deleted', 'All documents deleted successfully.');
|
|
262
|
+
loadDocuments();
|
|
263
|
+
} else {
|
|
264
|
+
showModal('error', 'Delete Failed', await res.json().error || 'Delete failed, please try again.');
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
showModal('error', 'Delete Failed', error.message || 'Delete failed, please try again.');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function dragStart(e) {
|
|
272
|
+
dragSrc = this;
|
|
273
|
+
e.dataTransfer.effectAllowed = "move";
|
|
274
|
+
this.classList.add("dragging");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function dragOver(e) {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
e.dataTransfer.dropEffect = "move";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function drop(e) {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
if (dragSrc !== this) {
|
|
285
|
+
// Swap positions
|
|
286
|
+
const draggedIndex = Array.from(tabsContainer.children).indexOf(dragSrc);
|
|
287
|
+
const targetIndex = Array.from(tabsContainer.children).indexOf(this);
|
|
288
|
+
|
|
289
|
+
if (draggedIndex < targetIndex) {
|
|
290
|
+
tabsContainer.insertBefore(dragSrc, this.nextSibling);
|
|
291
|
+
} else {
|
|
292
|
+
tabsContainer.insertBefore(dragSrc, this);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function dragEnd() {
|
|
298
|
+
this.classList.remove("dragging");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function refreshSideBar() {
|
|
302
|
+
try {
|
|
303
|
+
const modelWrapper = document.querySelector('.model-wrapper');
|
|
304
|
+
|
|
305
|
+
const res = await fetch("/models");
|
|
306
|
+
|
|
307
|
+
if (!res.ok) {
|
|
308
|
+
const error = await res.json();
|
|
309
|
+
showModal('error', 'Error Occurred!', error.error || 'Something went wrong, please try again.');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
modelWrapper.innerHTML = "";
|
|
314
|
+
const data = await res.json();
|
|
315
|
+
|
|
316
|
+
data.models.forEach(model => {
|
|
317
|
+
const modelDiv = document.createElement('div');
|
|
318
|
+
modelDiv.classList.add('model');
|
|
319
|
+
modelDiv.onclick = () => openModel(model.name);
|
|
320
|
+
modelDiv.innerHTML = `${model.name} <span id="${model.name}-doc-count">${model.count}</span>`;
|
|
321
|
+
modelWrapper.appendChild(modelDiv);
|
|
322
|
+
});
|
|
323
|
+
} catch (error) {
|
|
324
|
+
showModal('error', 'Error Occurred!', error.message || 'Something went wrong, please try again.');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function showModal(type, title, message) {
|
|
2
|
+
const popup = document.getElementById('popup');
|
|
3
|
+
const box = document.getElementById('popupBox');
|
|
4
|
+
const titleEl = document.getElementById('popupTitle');
|
|
5
|
+
const msgEl = document.getElementById('popupMessage');
|
|
6
|
+
|
|
7
|
+
box.className = 'popup-box';
|
|
8
|
+
if (type === 'warning') box.classList.add('warning');
|
|
9
|
+
else if (type === 'info') box.classList.add('info');
|
|
10
|
+
|
|
11
|
+
titleEl.textContent = title;
|
|
12
|
+
msgEl.textContent = message;
|
|
13
|
+
|
|
14
|
+
popup.style.display = 'flex';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function closePopup() {
|
|
18
|
+
document.getElementById('popup').style.display = 'none';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
window.onclick = function (e) {
|
|
22
|
+
const popup = document.getElementById('popup');
|
|
23
|
+
if (e.target === popup) {
|
|
24
|
+
closePopup();
|
|
25
|
+
}
|
|
26
|
+
}
|