fix server - agent
This commit is contained in:
@@ -38,6 +38,7 @@ class TaskRunner:
|
||||
manifest["appName"],
|
||||
manifest["version"],
|
||||
manifest_hash,
|
||||
manifest.get("openUrl"),
|
||||
)
|
||||
self.repository.update_task(
|
||||
task_id,
|
||||
|
||||
@@ -181,6 +181,7 @@ class AppManifest(CamelModel):
|
||||
app_id: str = Field(alias="appId")
|
||||
app_name: str = Field(alias="appName")
|
||||
version: str
|
||||
open_url: str | None = Field(default=None, alias="openUrl")
|
||||
architecture: str = "amd64"
|
||||
components: list[dict[str, Any]]
|
||||
signature: str | None = None
|
||||
|
||||
@@ -38,6 +38,7 @@ CREATE TABLE IF NOT EXISTS installed_apps (
|
||||
app_id TEXT PRIMARY KEY,
|
||||
app_name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
open_url TEXT,
|
||||
manifest_hash TEXT,
|
||||
status TEXT NOT NULL,
|
||||
installed_at TEXT NOT NULL,
|
||||
@@ -86,6 +87,15 @@ CREATE TABLE IF NOT EXISTS agent_config (
|
||||
"""
|
||||
|
||||
|
||||
def _ensure_column(connection: sqlite3.Connection, table: str, column: str, definition: str) -> None:
|
||||
columns = {
|
||||
row["name"]
|
||||
for row in connection.execute(f"PRAGMA table_info({table})").fetchall()
|
||||
}
|
||||
if column not in columns:
|
||||
connection.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
|
||||
|
||||
|
||||
def get_connection() -> sqlite3.Connection:
|
||||
connection = sqlite3.connect(settings.db_path, timeout=30)
|
||||
connection.row_factory = sqlite3.Row
|
||||
@@ -99,4 +109,4 @@ def initialize_database() -> None:
|
||||
settings.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
with get_connection() as connection:
|
||||
connection.executescript(SCHEMA)
|
||||
|
||||
_ensure_column(connection, "installed_apps", "open_url", "TEXT")
|
||||
|
||||
@@ -160,7 +160,7 @@ class Repository:
|
||||
with get_connection() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT app_id, app_name, version, manifest_hash, status, installed_at, updated_at
|
||||
SELECT app_id, app_name, version, open_url, manifest_hash, status, installed_at, updated_at
|
||||
FROM installed_apps
|
||||
ORDER BY app_name ASC
|
||||
"""
|
||||
@@ -173,22 +173,24 @@ class Repository:
|
||||
app_name: str,
|
||||
version: str,
|
||||
manifest_hash: str | None,
|
||||
open_url: str | None = None,
|
||||
status: str = "installed",
|
||||
) -> None:
|
||||
now = utc_now()
|
||||
with get_connection() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO installed_apps (app_id, app_name, version, manifest_hash, status, installed_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO installed_apps (app_id, app_name, version, open_url, manifest_hash, status, installed_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(app_id) DO UPDATE SET
|
||||
app_name = excluded.app_name,
|
||||
version = excluded.version,
|
||||
open_url = excluded.open_url,
|
||||
manifest_hash = excluded.manifest_hash,
|
||||
status = excluded.status,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(app_id, app_name, version, manifest_hash, status, now, now),
|
||||
(app_id, app_name, version, open_url, manifest_hash, status, now, now),
|
||||
)
|
||||
|
||||
def delete_installed_app(self, app_id: str) -> None:
|
||||
@@ -252,4 +254,3 @@ class Repository:
|
||||
|
||||
def export_manifest_hash(self, manifest: dict[str, Any]) -> str:
|
||||
return json.dumps(manifest, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ CREATE TABLE dbo.Applications
|
||||
Status NVARCHAR(50) NOT NULL
|
||||
CONSTRAINT DF_Applications_Status DEFAULT N'Draft',
|
||||
Notes NVARCHAR(500) NULL,
|
||||
OpenUrl NVARCHAR(500) NULL,
|
||||
CONSTRAINT FK_Applications_CreatedByUser
|
||||
FOREIGN KEY (CreatedByUserId) REFERENCES dbo.Users(Id),
|
||||
CONSTRAINT CK_Applications_Status CHECK (Status IN (N'Draft', N'Released', N'Archived')),
|
||||
|
||||
@@ -76,6 +76,7 @@ SELECT
|
||||
a.Description,
|
||||
a.Status,
|
||||
a.Notes,
|
||||
a.OpenUrl,
|
||||
a.CreatedAt,
|
||||
a.UpdatedAt,
|
||||
a.CreatedByUserId,
|
||||
@@ -94,6 +95,7 @@ GROUP BY
|
||||
a.Description,
|
||||
a.Status,
|
||||
a.Notes,
|
||||
a.OpenUrl,
|
||||
a.CreatedAt,
|
||||
a.UpdatedAt,
|
||||
a.CreatedByUserId,
|
||||
@@ -108,6 +110,7 @@ SELECT
|
||||
a.AppCode,
|
||||
a.AppName,
|
||||
a.AppVersion,
|
||||
a.OpenUrl AS AppOpenUrl,
|
||||
ap.PackageId,
|
||||
p.PackageCode,
|
||||
p.PackageName,
|
||||
|
||||
@@ -412,6 +412,7 @@
|
||||
name: trigger.dataset.appName || '',
|
||||
version: trigger.dataset.appVersion || '',
|
||||
status: trigger.dataset.appStatus || 'Draft',
|
||||
openUrl: trigger.dataset.appOpenUrl || '',
|
||||
notes: trigger.dataset.appNotes || '',
|
||||
packages: parseJsonAttribute(trigger.dataset.appPackages, [])
|
||||
};
|
||||
@@ -446,6 +447,7 @@
|
||||
setEditAppField(form, 'appName', app.name);
|
||||
setEditAppField(form, 'appVersion', app.version);
|
||||
setEditAppField(form, 'status', app.status);
|
||||
setEditAppField(form, 'openUrl', app.openUrl);
|
||||
setEditAppField(form, 'notes', app.notes);
|
||||
|
||||
form.querySelectorAll('[data-edit-app-package]').forEach((checkbox) => {
|
||||
|
||||
@@ -844,6 +844,24 @@ function normalizeApplicationStatus(value) {
|
||||
return ['Draft', 'Released', 'Archived'].includes(value) ? value : 'Draft';
|
||||
}
|
||||
|
||||
function normalizeApplicationOpenUrl(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return '';
|
||||
|
||||
const candidate = /^https?:\/\//i.test(text) ? text : `http://${text}`;
|
||||
|
||||
try {
|
||||
const parsed = new URL(candidate);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed.href.replace(/\/+$/, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isInstallerIdentifier(value) {
|
||||
return installerIdentifierPattern.test(String(value || '').trim());
|
||||
}
|
||||
@@ -1297,7 +1315,8 @@ app.get('/api/apps', asyncRoute(async (req, res) => {
|
||||
appName: application.name,
|
||||
version: application.version,
|
||||
status: application.status,
|
||||
packageCount: application.packageCount
|
||||
packageCount: application.packageCount,
|
||||
openUrl: application.openUrl
|
||||
}))
|
||||
});
|
||||
}));
|
||||
@@ -1316,6 +1335,7 @@ app.get('/api/apps/:appCode', asyncRoute(async (req, res) => {
|
||||
version: application.version,
|
||||
status: application.status,
|
||||
packageCount: application.packageCount,
|
||||
openUrl: application.openUrl,
|
||||
packages: application.packages
|
||||
});
|
||||
}));
|
||||
@@ -1839,7 +1859,7 @@ app.get('/applications', asyncRoute(async (req, res) => {
|
||||
app.get('/applications/export.csv', asyncRoute(async (req, res) => {
|
||||
const applications = await repository.listApplications();
|
||||
const rows = [
|
||||
['AppCode', 'AppName', 'AppVersion', 'Status', 'CreatedAt', 'CreatedBy', 'PackageCount', 'Packages', 'Notes'],
|
||||
['AppCode', 'AppName', 'AppVersion', 'Status', 'CreatedAt', 'CreatedBy', 'PackageCount', 'Packages', 'OpenUrl', 'Notes'],
|
||||
...applications.map((application) => [
|
||||
application.code,
|
||||
application.name,
|
||||
@@ -1849,6 +1869,7 @@ app.get('/applications/export.csv', asyncRoute(async (req, res) => {
|
||||
application.createdBy,
|
||||
application.packageCount,
|
||||
application.packages.map((packageItem) => `${packageItem.code}@${packageItem.selectedVersion}`).join('; '),
|
||||
application.openUrl,
|
||||
application.notes
|
||||
])
|
||||
];
|
||||
@@ -1860,12 +1881,18 @@ app.post('/applications', asyncRoute(async (req, res) => {
|
||||
const appCode = String(req.body.appCode || '').trim();
|
||||
const appName = String(req.body.appName || '').trim();
|
||||
const appVersion = String(req.body.appVersion || '').trim();
|
||||
const openUrl = normalizeApplicationOpenUrl(req.body.openUrl);
|
||||
|
||||
if (!appCode || !appName || !appVersion) {
|
||||
redirectWithNotice(res, '/builder', 'warning', 'Vui lòng nhập App code, App name và App version.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (openUrl === null) {
|
||||
redirectWithNotice(res, '/builder', 'warning', 'Open URL khong hop le. Hay nhap URL http/https, vi du http://127.0.0.1:5000.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInstallerIdentifier(appCode)) {
|
||||
redirectWithNotice(res, '/builder', 'warning', `App code khong hop le. ${installerIdentifierHint}`);
|
||||
return;
|
||||
@@ -1882,6 +1909,7 @@ app.post('/applications', asyncRoute(async (req, res) => {
|
||||
appName,
|
||||
appVersion,
|
||||
notes: req.body.notes,
|
||||
openUrl,
|
||||
status: normalizeApplicationStatus(req.body.status),
|
||||
createdByUserId: req.currentUser.id,
|
||||
packages: getSelectedApplicationPackages(req.body)
|
||||
@@ -1904,12 +1932,18 @@ app.post('/applications/:id/edit', asyncRoute(async (req, res) => {
|
||||
const appCode = String(req.body.appCode || '').trim();
|
||||
const appName = String(req.body.appName || '').trim();
|
||||
const appVersion = String(req.body.appVersion || '').trim();
|
||||
const openUrl = normalizeApplicationOpenUrl(req.body.openUrl);
|
||||
|
||||
if (!appCode || !appName || !appVersion) {
|
||||
redirectWithNotice(res, returnTo, 'warning', 'Vui lòng nhập App code, App name và App version.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (openUrl === null) {
|
||||
redirectWithNotice(res, returnTo, 'warning', 'Open URL khong hop le. Hay nhap URL http/https, vi du http://127.0.0.1:5000.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInstallerIdentifier(appCode)) {
|
||||
redirectWithNotice(res, returnTo, 'warning', `App code khong hop le. ${installerIdentifierHint}`);
|
||||
return;
|
||||
@@ -1927,6 +1961,7 @@ app.post('/applications/:id/edit', asyncRoute(async (req, res) => {
|
||||
appName,
|
||||
appVersion,
|
||||
notes: req.body.notes,
|
||||
openUrl,
|
||||
status: normalizeApplicationStatus(req.body.status),
|
||||
packages: getSelectedApplicationPackages(req.body)
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ const PLACEHOLDER_PASSWORD_HASH = 'ui-placeholder-password-hash';
|
||||
const EMAIL_CONFIRMATION_EXPIRES_MS = Number(process.env.EMAIL_CONFIRMATION_EXPIRES_MS || 1000 * 60 * 60 * 24);
|
||||
|
||||
let emailConfirmationSchemaPromise;
|
||||
let applicationOpenUrlSchemaPromise;
|
||||
|
||||
function toDateInputValue(value) {
|
||||
const date = value ? new Date(value) : new Date();
|
||||
@@ -185,6 +186,86 @@ async function ensureEmailConfirmationSchema() {
|
||||
return emailConfirmationSchemaPromise;
|
||||
}
|
||||
|
||||
async function ensureApplicationOpenUrlSchema() {
|
||||
if (!applicationOpenUrlSchemaPromise) {
|
||||
applicationOpenUrlSchemaPromise = getPool().then((pool) => pool.request().query(`
|
||||
IF COL_LENGTH(N'dbo.Applications', N'OpenUrl') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Applications
|
||||
ADD OpenUrl NVARCHAR(500) NULL;
|
||||
END;
|
||||
|
||||
EXEC(N'
|
||||
CREATE OR ALTER VIEW dbo.vw_ApplicationList
|
||||
AS
|
||||
SELECT
|
||||
a.Id,
|
||||
a.AppCode,
|
||||
a.AppName,
|
||||
a.AppVersion,
|
||||
a.Description,
|
||||
a.Status,
|
||||
a.Notes,
|
||||
a.OpenUrl,
|
||||
a.CreatedAt,
|
||||
a.UpdatedAt,
|
||||
a.CreatedByUserId,
|
||||
u.Username AS CreatedByUsername,
|
||||
COUNT_BIG(ap.Id) AS PackageCount
|
||||
FROM dbo.Applications AS a
|
||||
INNER JOIN dbo.Users AS u
|
||||
ON u.Id = a.CreatedByUserId
|
||||
LEFT JOIN dbo.ApplicationPackages AS ap
|
||||
ON ap.ApplicationId = a.Id
|
||||
GROUP BY
|
||||
a.Id,
|
||||
a.AppCode,
|
||||
a.AppName,
|
||||
a.AppVersion,
|
||||
a.Description,
|
||||
a.Status,
|
||||
a.Notes,
|
||||
a.OpenUrl,
|
||||
a.CreatedAt,
|
||||
a.UpdatedAt,
|
||||
a.CreatedByUserId,
|
||||
u.Username;
|
||||
');
|
||||
|
||||
EXEC(N'
|
||||
CREATE OR ALTER VIEW dbo.vw_ApplicationPackageDetails
|
||||
AS
|
||||
SELECT
|
||||
ap.Id,
|
||||
ap.ApplicationId,
|
||||
a.AppCode,
|
||||
a.AppName,
|
||||
a.AppVersion,
|
||||
a.OpenUrl AS AppOpenUrl,
|
||||
ap.PackageId,
|
||||
p.PackageCode,
|
||||
p.PackageName,
|
||||
p.PackageType,
|
||||
ap.SelectedVersionId,
|
||||
pv.Version AS SelectedVersion,
|
||||
pv.FilePath,
|
||||
pv.DockerImage,
|
||||
ap.AddedAt,
|
||||
ap.Notes
|
||||
FROM dbo.ApplicationPackages AS ap
|
||||
INNER JOIN dbo.Applications AS a
|
||||
ON a.Id = ap.ApplicationId
|
||||
INNER JOIN dbo.Packages AS p
|
||||
ON p.Id = ap.PackageId
|
||||
LEFT JOIN dbo.PackageVersions AS pv
|
||||
ON pv.Id = ap.SelectedVersionId;
|
||||
');
|
||||
`));
|
||||
}
|
||||
|
||||
return applicationOpenUrlSchemaPromise;
|
||||
}
|
||||
|
||||
function normalizePackageStatus(isActive) {
|
||||
return isActive ? 'Active' : 'Archived';
|
||||
}
|
||||
@@ -242,6 +323,7 @@ function mapApplicationRow(row) {
|
||||
createdAt: formatDate(row.CreatedAt),
|
||||
createdBy: row.CreatedByUsername || '',
|
||||
notes: row.Notes || row.Description || '',
|
||||
openUrl: row.OpenUrl || '',
|
||||
packageCount: Number(row.PackageCount || 0),
|
||||
packages: []
|
||||
};
|
||||
@@ -251,6 +333,7 @@ function mapApplicationPackageRow(row) {
|
||||
return {
|
||||
id: String(row.Id),
|
||||
applicationId: String(row.ApplicationId),
|
||||
appOpenUrl: row.AppOpenUrl || '',
|
||||
packageId: String(row.PackageId),
|
||||
code: row.PackageCode,
|
||||
name: row.PackageName,
|
||||
@@ -759,6 +842,7 @@ async function getPackageById(id) {
|
||||
}
|
||||
|
||||
async function listApplications() {
|
||||
await ensureApplicationOpenUrlSchema();
|
||||
const pool = await getPool();
|
||||
const result = await pool.request().query(`
|
||||
SELECT *
|
||||
@@ -771,6 +855,7 @@ async function listApplications() {
|
||||
}
|
||||
|
||||
async function listApplicationPackages(applicationId) {
|
||||
await ensureApplicationOpenUrlSchema();
|
||||
const pool = await getPool();
|
||||
const request = pool.request();
|
||||
let whereClause = '';
|
||||
@@ -816,6 +901,7 @@ async function attachApplicationPackages(applications) {
|
||||
}
|
||||
|
||||
async function getApplicationById(id) {
|
||||
await ensureApplicationOpenUrlSchema();
|
||||
const pool = await getPool();
|
||||
const appResult = await pool.request()
|
||||
.input('Id', sql.NVarChar(100), id)
|
||||
@@ -834,12 +920,13 @@ async function getApplicationById(id) {
|
||||
}
|
||||
|
||||
async function getApplicationManifest(appCode, version, baseUrl) {
|
||||
await ensureApplicationOpenUrlSchema();
|
||||
const pool = await getPool();
|
||||
const appResult = await pool.request()
|
||||
.input('AppCode', sql.NVarChar(100), String(appCode || '').trim())
|
||||
.input('AppVersion', sql.NVarChar(50), String(version || '').trim())
|
||||
.query(`
|
||||
SELECT TOP (1) Id, AppCode, AppName, AppVersion
|
||||
SELECT TOP (1) Id, AppCode, AppName, AppVersion, OpenUrl
|
||||
FROM dbo.Applications
|
||||
WHERE (CONVERT(NVARCHAR(36), Id) = @AppCode OR AppCode = @AppCode)
|
||||
AND AppVersion = @AppVersion
|
||||
@@ -911,6 +998,7 @@ async function getApplicationManifest(appCode, version, baseUrl) {
|
||||
appId: String(appRow.Id),
|
||||
appName: appRow.AppName,
|
||||
version: appRow.AppVersion,
|
||||
openUrl: appRow.OpenUrl || '',
|
||||
architecture: 'amd64',
|
||||
components
|
||||
};
|
||||
@@ -1199,6 +1287,7 @@ async function deletePackageVersion(packageVersionId) {
|
||||
}
|
||||
|
||||
async function createApplication(input) {
|
||||
await ensureApplicationOpenUrlSchema();
|
||||
const pool = await getPool();
|
||||
const transaction = new sql.Transaction(pool);
|
||||
|
||||
@@ -1210,12 +1299,13 @@ async function createApplication(input) {
|
||||
.input('AppName', sql.NVarChar(200), input.appName)
|
||||
.input('AppVersion', sql.NVarChar(50), input.appVersion)
|
||||
.input('Notes', sql.NVarChar(500), input.notes || null)
|
||||
.input('OpenUrl', sql.NVarChar(500), input.openUrl || null)
|
||||
.input('CreatedByUserId', sql.UniqueIdentifier, input.createdByUserId)
|
||||
.input('Status', sql.NVarChar(50), normalizeApplicationStatus(input.status))
|
||||
.query(`
|
||||
INSERT dbo.Applications (AppCode, AppName, AppVersion, Notes, CreatedByUserId, Status)
|
||||
INSERT dbo.Applications (AppCode, AppName, AppVersion, Notes, OpenUrl, CreatedByUserId, Status)
|
||||
OUTPUT inserted.Id
|
||||
VALUES (@AppCode, @AppName, @AppVersion, @Notes, @CreatedByUserId, @Status);
|
||||
VALUES (@AppCode, @AppName, @AppVersion, @Notes, @OpenUrl, @CreatedByUserId, @Status);
|
||||
`);
|
||||
|
||||
const applicationId = String(appResult.recordset[0].Id);
|
||||
@@ -1244,6 +1334,7 @@ async function createApplication(input) {
|
||||
}
|
||||
|
||||
async function updateApplication(input) {
|
||||
await ensureApplicationOpenUrlSchema();
|
||||
const pool = await getPool();
|
||||
const transaction = new sql.Transaction(pool);
|
||||
|
||||
@@ -1270,6 +1361,7 @@ async function updateApplication(input) {
|
||||
.input('AppName', sql.NVarChar(200), input.appName)
|
||||
.input('AppVersion', sql.NVarChar(50), input.appVersion)
|
||||
.input('Notes', sql.NVarChar(500), input.notes || null)
|
||||
.input('OpenUrl', sql.NVarChar(500), input.openUrl || null)
|
||||
.input('Status', sql.NVarChar(50), normalizeApplicationStatus(input.status))
|
||||
.query(`
|
||||
UPDATE dbo.Applications
|
||||
@@ -1277,6 +1369,7 @@ async function updateApplication(input) {
|
||||
AppName = @AppName,
|
||||
AppVersion = @AppVersion,
|
||||
Notes = @Notes,
|
||||
OpenUrl = @OpenUrl,
|
||||
Status = @Status,
|
||||
UpdatedAt = SYSUTCDATETIME()
|
||||
OUTPUT inserted.Id
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
data-app-name="<%= application.name %>"
|
||||
data-app-version="<%= application.version %>"
|
||||
data-app-status="<%= application.status %>"
|
||||
data-app-open-url="<%= application.openUrl %>"
|
||||
data-app-notes="<%= application.notes %>"
|
||||
data-app-packages="<%= JSON.stringify(application.packages.map((pkg) => ({ packageId: pkg.packageId, selectedVersionId: pkg.selectedVersionId }))) %>"
|
||||
>
|
||||
@@ -53,6 +54,7 @@
|
||||
<div><dt>Package count</dt><dd><%= application.packageCount %></dd></div>
|
||||
<div><dt>Created date</dt><dd><%= application.createdAt %></dd></div>
|
||||
<div><dt>Created by</dt><dd><%= application.createdBy %></dd></div>
|
||||
<div><dt>Open URL</dt><dd class="mono"><%= application.openUrl || '-' %></dd></div>
|
||||
<div><dt>Status</dt><dd><span class="badge <%= helpers.statusClass(application.status) %>"><%= application.status %></span></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
@@ -95,6 +97,7 @@
|
||||
data-app-name="<%= application.name %>"
|
||||
data-app-version="<%= application.version %>"
|
||||
data-app-status="<%= application.status %>"
|
||||
data-app-open-url="<%= application.openUrl %>"
|
||||
data-app-notes="<%= application.notes %>"
|
||||
data-app-packages="<%= JSON.stringify(application.packages.map((item) => ({ packageId: item.packageId, selectedVersionId: item.selectedVersionId }))) %>"
|
||||
>
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
data-app-name="<%= item.name %>"
|
||||
data-app-version="<%= item.version %>"
|
||||
data-app-status="<%= item.status %>"
|
||||
data-app-open-url="<%= item.openUrl %>"
|
||||
data-app-notes="<%= item.notes %>"
|
||||
data-app-packages="<%= JSON.stringify(item.packages.map((pkg) => ({ packageId: pkg.packageId, selectedVersionId: pkg.selectedVersionId }))) %>"
|
||||
>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<span>App name</span>
|
||||
<input type="text" name="appName" required>
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Open URL</span>
|
||||
<input type="text" name="openUrl" placeholder="http://127.0.0.1:5000">
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Notes</span>
|
||||
<textarea name="notes"></textarea>
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<span>App name</span>
|
||||
<input type="text" name="appName" required data-edit-app-field="appName">
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Open URL</span>
|
||||
<input type="text" name="openUrl" placeholder="http://127.0.0.1:5000" data-edit-app-field="openUrl">
|
||||
</label>
|
||||
<label class="form-field full">
|
||||
<span>Notes</span>
|
||||
<textarea name="notes" data-edit-app-field="notes"></textarea>
|
||||
|
||||
Reference in New Issue
Block a user