diff --git a/packages/api/src/controllers/apps.js b/packages/api/src/controllers/apps.js
new file mode 100644
index 000000000..655f13b51
--- /dev/null
+++ b/packages/api/src/controllers/apps.js
@@ -0,0 +1,100 @@
+const fs = require('fs-extra');
+const path = require('path');
+const { appdir } = require('../utility/directories');
+const socket = require('../utility/socket');
+
+module.exports = {
+ folders_meta: true,
+ async folders() {
+ const folders = await fs.readdir(appdir());
+ return [
+ ...folders.map(name => ({
+ name,
+ })),
+ ];
+ },
+
+ createFolder_meta: true,
+ async createFolder({ folder }) {
+ await fs.mkdir(path.join(appdir(), folder));
+ socket.emitChanged('app-folders-changed');
+ return true;
+ },
+
+ files_meta: true,
+ async files({ folder }) {
+ const dir = path.join(appdir(), folder);
+ if (!(await fs.exists(dir))) return [];
+ const files = await fs.readdir(dir);
+
+ function fileType(ext, type) {
+ return files
+ .filter(name => name.endsWith(ext))
+ .map(name => ({
+ name: name.slice(0, -ext.length),
+ label: path.parse(name.slice(0, -ext.length)).base,
+ type,
+ }));
+ }
+
+ function refsType() {
+ return files
+ .filter(name => name == 'virtual-references.json')
+ .map(name => ({
+ name: 'virtual-references.json',
+ label: 'virtual-references.json',
+ type: 'vfk',
+ }));
+ }
+
+ return [...refsType(), ...fileType('.command.sql', 'command.sql'), ...fileType('.query.sql', 'query.sql')];
+ },
+
+ refreshFiles_meta: true,
+ async refreshFiles({ folder }) {
+ socket.emitChanged(`app-files-changed-${folder}`);
+ },
+
+ refreshFolders_meta: true,
+ async refreshFolders() {
+ socket.emitChanged(`app-folders-changed`);
+ },
+
+ deleteFile_meta: true,
+ async deleteFile({ folder, file, fileType }) {
+ await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
+ socket.emitChanged(`app-files-changed-${folder}`);
+ },
+
+ renameFile_meta: true,
+ async renameFile({ folder, file, newFile, fileType }) {
+ await fs.rename(
+ path.join(path.join(appdir(), folder), `${file}.${fileType}`),
+ path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
+ );
+ socket.emitChanged(`app-files-changed-${folder}`);
+ },
+
+ renameFolder_meta: true,
+ async renameFolder({ folder, newFolder }) {
+ const uniqueName = await this.getNewAppFolder({ name: newFolder });
+ await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
+ socket.emitChanged(`app-folders-changed`);
+ },
+
+ deleteFolder_meta: true,
+ async deleteFolder({ folder }) {
+ if (!folder) throw new Error('Missing folder parameter');
+ await fs.rmdir(path.join(appdir(), folder), { recursive: true });
+ socket.emitChanged(`app-folders-changed`);
+ },
+
+ async getNewAppFolder({ name }) {
+ if (!(await fs.exists(path.join(appdir(), name)))) return name;
+ let index = 2;
+ while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
+ index += 1;
+ }
+ return `${name}${index}`;
+ },
+};
diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js
index 78a297784..8c8319a7d 100644
--- a/packages/api/src/controllers/files.js
+++ b/packages/api/src/controllers/files.js
@@ -1,7 +1,7 @@
const uuidv1 = require('uuid/v1');
const fs = require('fs-extra');
const path = require('path');
-const { filesdir, archivedir, resolveArchiveFolder, uploadsdir } = require('../utility/directories');
+const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir } = require('../utility/directories');
const getChartExport = require('../utility/getChartExport');
const hasPermission = require('../utility/hasPermission');
const socket = require('../utility/socket');
@@ -74,6 +74,11 @@ module.exports = {
encoding: 'utf-8',
});
return deserialize(format, text);
+ } else if (folder.startsWith('app:')) {
+ const text = await fs.readFile(path.join(appdir(), folder.substring('app:'.length), file), {
+ encoding: 'utf-8',
+ });
+ return deserialize(format, text);
} else {
if (!hasPermission(`files/${folder}/read`)) return null;
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
@@ -88,6 +93,10 @@ module.exports = {
await fs.writeFile(path.join(dir, file), serialize(format, data));
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
return true;
+ } else if (folder.startsWith('app:')) {
+ await fs.writeFile(path.join(appdir(), folder.substring('app:'.length), file), serialize(format, data));
+ socket.emitChanged(`app-files-changed-${folder.substring('app:'.length)}`);
+ return true;
} else {
if (!hasPermission(`files/${folder}/write`)) return false;
const dir = path.join(filesdir(), folder);
diff --git a/packages/api/src/main.js b/packages/api/src/main.js
index 5cb5ec46e..2ad5588de 100644
--- a/packages/api/src/main.js
+++ b/packages/api/src/main.js
@@ -19,6 +19,7 @@ const runners = require('./controllers/runners');
const jsldata = require('./controllers/jsldata');
const config = require('./controllers/config');
const archive = require('./controllers/archive');
+const apps = require('./controllers/apps');
const uploads = require('./controllers/uploads');
const plugins = require('./controllers/plugins');
const files = require('./controllers/files');
@@ -157,6 +158,7 @@ function useAllControllers(app, electron) {
useController(app, electron, '/files', files);
useController(app, electron, '/scheduler', scheduler);
useController(app, electron, '/query-history', queryHistory);
+ useController(app, electron, '/apps', apps);
}
function initializeElectronSender(electronSender) {
diff --git a/packages/api/src/utility/directories.js b/packages/api/src/utility/directories.js
index 9af898e1e..35b5c8874 100644
--- a/packages/api/src/utility/directories.js
+++ b/packages/api/src/utility/directories.js
@@ -38,6 +38,7 @@ const rundir = dirFunc('run', true);
const uploadsdir = dirFunc('uploads', true);
const pluginsdir = dirFunc('plugins');
const archivedir = dirFunc('archive');
+const appdir = dirFunc('apps');
const filesdir = dirFunc('files');
function packagedPluginsDir() {
@@ -103,6 +104,7 @@ module.exports = {
rundir,
uploadsdir,
archivedir,
+ appdir,
ensureDirectory,
pluginsdir,
filesdir,
diff --git a/packages/web/src/appobj/AppFileAppObject.svelte b/packages/web/src/appobj/AppFileAppObject.svelte
new file mode 100644
index 000000000..e15251038
--- /dev/null
+++ b/packages/web/src/appobj/AppFileAppObject.svelte
@@ -0,0 +1,123 @@
+
+
+
+
+
diff --git a/packages/web/src/appobj/AppFolderAppObject.svelte b/packages/web/src/appobj/AppFolderAppObject.svelte
new file mode 100644
index 000000000..64e0bc8ac
--- /dev/null
+++ b/packages/web/src/appobj/AppFolderAppObject.svelte
@@ -0,0 +1,64 @@
+
+
+
+
+ ($currentApplication = data.name)}
+ menu={createMenu}
+/>
diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte
index 83e24c4a1..bc2b3cbf8 100644
--- a/packages/web/src/appobj/ArchiveFileAppObject.svelte
+++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte
@@ -81,7 +81,7 @@
markArchiveFileAsDataSheet,
markArchiveFileAsReadonly,
} from '../utility/archiveTools';
-import { apiCall } from '../utility/api';
+ import { apiCall } from '../utility/api';
export let data;
diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts
index b5b051e07..85b79c5c8 100644
--- a/packages/web/src/commands/stdCommands.ts
+++ b/packages/web/src/commands/stdCommands.ts
@@ -128,6 +128,23 @@ registerCommand({
},
});
+registerCommand({
+ id: 'new.application',
+ category: 'New',
+ icon: 'img app',
+ name: 'Application',
+ onClick: () => {
+ showModal(InputTextModal, {
+ value: '',
+ label: 'New application name',
+ header: 'Create application',
+ onConfirm: async folder => {
+ apiCall('apps/create-folder', { folder });
+ },
+ });
+ },
+});
+
registerCommand({
id: 'new.table',
category: 'New',
diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte
index 2c79d71f9..90aebcfeb 100644
--- a/packages/web/src/icons/FontIcon.svelte
+++ b/packages/web/src/icons/FontIcon.svelte
@@ -26,6 +26,7 @@
'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back',
+ 'icon app': 'mdi mdi-layers-triple',
'icon columns': 'mdi mdi-view-column',
'icon columns-outline': 'mdi mdi-view-column-outline',
@@ -126,6 +127,9 @@
'img diagram': 'mdi mdi-graph color-icon-blue',
'img yaml': 'mdi mdi-code-brackets color-icon-red',
'img compare': 'mdi mdi-compare color-icon-red',
+ 'img app': 'mdi mdi-layers-triple color-icon-magenta',
+ 'img app-command': 'mdi mdi-flash color-icon-green',
+ 'img app-query': 'mdi mdi-view-comfy color-icon-magenta',
'img add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red',
diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts
index 068ba5074..0266b9b89 100644
--- a/packages/web/src/stores.ts
+++ b/packages/web/src/stores.ts
@@ -64,6 +64,7 @@ export const openedModals = writable([]);
export const openedSnackbars = writable([]);
export const nullStore = readable(null, () => {});
export const currentArchive = writableWithStorage('default', 'currentArchive');
+export const currentApplication = writableWithStorage(null, 'currentApplication');
export const isFileDragActive = writable(false);
export const selectedCellsCallback = writable(null);
export const loadingPluginStore = writable({
diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts
index 3fcf6fa6f..d688b929f 100644
--- a/packages/web/src/utility/metadataLoaders.ts
+++ b/packages/web/src/utility/metadataLoaders.ts
@@ -103,6 +103,18 @@ const archiveFilesLoader = ({ folder }) => ({
reloadTrigger: `archive-files-changed-${folder}`,
});
+const appFoldersLoader = () => ({
+ url: 'apps/folders',
+ params: {},
+ reloadTrigger: `app-folders-changed`,
+});
+
+const appFilesLoader = ({ folder }) => ({
+ url: 'apps/files',
+ params: { folder },
+ reloadTrigger: `app-files-changed-${folder}`,
+});
+
const serverStatusLoader = () => ({
url: 'server-connections/server-status',
params: {},
@@ -401,6 +413,20 @@ export function useArchiveFolders(args = {}) {
return useCore(archiveFoldersLoader, args);
}
+export function getAppFiles(args) {
+ return getCore(appFilesLoader, args);
+}
+export function useAppFiles(args) {
+ return useCore(appFilesLoader, args);
+}
+
+export function getAppFolders(args = {}) {
+ return getCore(appFoldersLoader, args);
+}
+export function useAppFolders(args = {}) {
+ return useCore(appFoldersLoader, args);
+}
+
export function getInstalledPlugins(args = {}) {
return getCore(installedPluginsLoader, args) || [];
}
diff --git a/packages/web/src/widgets/AppFilesList.svelte b/packages/web/src/widgets/AppFilesList.svelte
new file mode 100644
index 000000000..3206586a4
--- /dev/null
+++ b/packages/web/src/widgets/AppFilesList.svelte
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ fileName: file.name,
+ folderName: folder,
+ fileType: file.type,
+ fileLabel: file.label,
+ }))}
+ groupFunc={data => APP_LABELS[data.fileType] || 'App config'}
+ module={appFileAppObject}
+ {filter}
+ />
+
diff --git a/packages/web/src/widgets/AppFolderList.svelte b/packages/web/src/widgets/AppFolderList.svelte
new file mode 100644
index 000000000..84aa0871c
--- /dev/null
+++ b/packages/web/src/widgets/AppFolderList.svelte
@@ -0,0 +1,39 @@
+
+
+
+
+
+ runCommand('new.application')} title="Create new application">
+
+
+
+
+
+
+
+
+
diff --git a/packages/web/src/widgets/AppWidget.svelte b/packages/web/src/widgets/AppWidget.svelte
new file mode 100644
index 000000000..13074843e
--- /dev/null
+++ b/packages/web/src/widgets/AppWidget.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/web/src/widgets/WidgetContainer.svelte b/packages/web/src/widgets/WidgetContainer.svelte
index 14802429c..c53900b52 100644
--- a/packages/web/src/widgets/WidgetContainer.svelte
+++ b/packages/web/src/widgets/WidgetContainer.svelte
@@ -6,6 +6,7 @@
import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte';
+ import AppWidget from './AppWidget.svelte';
@@ -25,3 +26,6 @@
{#if $selectedWidget == 'cell-data'}
{/if}
+{#if $selectedWidget == 'app'}
+
+{/if}
diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte
index 440bca657..29a88a07b 100644
--- a/packages/web/src/widgets/WidgetIconPanel.svelte
+++ b/packages/web/src/widgets/WidgetIconPanel.svelte
@@ -40,6 +40,11 @@
name: 'cell-data',
title: 'Selected cell data detail view',
},
+ {
+ icon: 'icon app',
+ name: 'app',
+ title: 'Application layers',
+ },
// {
// icon: 'icon settings',
// name: 'settings',