增加安装app时条件判断,符合条件才能安装
This commit is contained in:
parent
da162f3cb2
commit
70e2158dd6
@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getCachedDocumentResource } from 'jingrow-ui';
|
import { getCachedDocumentResource, createResource } from 'jingrow-ui';
|
||||||
import { defineAsyncComponent, h } from 'vue';
|
import { defineAsyncComponent, h } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { renderDialog } from '../../utils/components';
|
import { renderDialog } from '../../utils/components';
|
||||||
@ -34,47 +34,114 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
show: true
|
show: true,
|
||||||
|
currentApp: null,
|
||||||
|
retryCount: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
resources: {
|
||||||
|
// 添加API资源定义:检查应用是否可安装
|
||||||
|
checkAppInstallable() {
|
||||||
|
return {
|
||||||
|
url: 'jcloud.api.site.check_app_installable',
|
||||||
|
params: {
|
||||||
|
name: this.site,
|
||||||
|
app: this.currentApp
|
||||||
|
},
|
||||||
|
auto: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
$site() {
|
$site() {
|
||||||
return getCachedDocumentResource('Site', this.site);
|
return getCachedDocumentResource('Site', this.site);
|
||||||
},
|
},
|
||||||
listOptions() {
|
listOptions() {
|
||||||
const handleInstall = row => {
|
const handleInstall = async row => {
|
||||||
if (this.$site.installApp.loading) return;
|
if (this.$site.installApp.loading) return;
|
||||||
|
|
||||||
if (row.plans) {
|
// 设置当前应用以便资源可以使用
|
||||||
this.show = false;
|
this.currentApp = row.app;
|
||||||
|
this.retryCount = 0
|
||||||
import('./SiteAppPlanSelectDialog.vue').then(module => {
|
|
||||||
const SiteAppPlanSelectDialog = module.default;
|
// 打印关键变量进行调试
|
||||||
|
console.log('=== 安装应用调试信息 ===');
|
||||||
|
console.log('站点名称:', this.site);
|
||||||
|
console.log('应用ID:', this.currentApp);
|
||||||
|
console.log('站点对象:', this.$site);
|
||||||
|
console.log('站点plan:', this.$site.pg?.plan);
|
||||||
|
console.log('=== 调试信息结束 ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试获取应用安装权限信息,最多重试2次
|
||||||
|
let checkResult = null;
|
||||||
|
let apiCalled = false;
|
||||||
|
|
||||||
|
while (!checkResult && this.retryCount < 3) {
|
||||||
|
console.log(`正在调用check_app_installable API... (尝试 ${this.retryCount + 1}/3)`);
|
||||||
|
apiCalled = true;
|
||||||
|
|
||||||
const component = h(SiteAppPlanSelectDialog, {
|
await this.$resources.checkAppInstallable.submit();
|
||||||
app: row,
|
|
||||||
currentPlan: null,
|
console.log('API调用完成,状态:', {
|
||||||
onPlanSelected: plan => {
|
loading: this.$resources.checkAppInstallable.loading,
|
||||||
handleAppInstall(row, plan);
|
error: this.$resources.checkAppInstallable.error
|
||||||
},
|
|
||||||
"onPlan-selected": plan => {
|
|
||||||
handleAppInstall(row, plan);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
renderDialog(component);
|
// 获取检查结果
|
||||||
}).catch(err => {
|
checkResult = this.$resources.checkAppInstallable.data;
|
||||||
toast.error('加载组件失败');
|
console.log('API返回结果:', checkResult);
|
||||||
this.show = true;
|
|
||||||
});
|
// 如果返回结果为null且未达到最大重试次数,等待500ms后重试
|
||||||
} else {
|
if (!checkResult && this.retryCount < 2) {
|
||||||
toast.promise(
|
this.retryCount++;
|
||||||
this.$site.installApp.submit({
|
console.log(`API返回null,将在500ms后重试(${this.retryCount}/2)...`);
|
||||||
app: row.app
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查返回结果是否有效
|
||||||
|
if (!checkResult) {
|
||||||
|
console.error('API返回结果无效,已达到最大重试次数');
|
||||||
|
toast.error('应用安装权限验证失败,安装已取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查应用是否可安装
|
||||||
|
console.log('应用可安装?', checkResult.installable);
|
||||||
|
console.log('required_plan_level:', checkResult.required_plan_level);
|
||||||
|
console.log('current_plan_level:', checkResult.current_plan_level);
|
||||||
|
|
||||||
|
// 如果结果显示不可安装,显示错误信息并退出
|
||||||
|
if (checkResult.installable !== true) {
|
||||||
|
console.log('应用不可安装,显示错误信息');
|
||||||
|
|
||||||
|
const subscriptionTypeMap = {
|
||||||
|
2: 'Pro',
|
||||||
|
3: 'Business',
|
||||||
|
4: 'Enterprise',
|
||||||
|
5: 'Ultimate'
|
||||||
|
};
|
||||||
|
|
||||||
|
const requiredPlan = subscriptionTypeMap[checkResult.required_plan_level] ||
|
||||||
|
`Level ${checkResult.required_plan_level}`;
|
||||||
|
|
||||||
|
toast.error(`无法安装此应用,需要${requiredPlan}或更高级别的站点计划。请先升级您的站点计划。`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果可安装,执行安装
|
||||||
|
console.log('应用可安装,开始安装流程');
|
||||||
|
await toast.promise(
|
||||||
|
this.$site.installApp.submit({
|
||||||
|
app: row.app
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: '正在创建安装任务...',
|
loading: '正在创建安装任务...',
|
||||||
success: jobId => {
|
success: jobId => {
|
||||||
|
console.log('安装任务创建成功,jobId:', jobId);
|
||||||
this.$emit('installed');
|
this.$emit('installed');
|
||||||
this.show = false;
|
this.show = false;
|
||||||
|
|
||||||
@ -87,58 +154,28 @@ export default {
|
|||||||
id: String(jobId)
|
id: String(jobId)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
console.error('导航到作业页面失败:', err);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('处理作业ID时出错:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '应用安装任务已创建';
|
return '应用安装任务已创建';
|
||||||
},
|
},
|
||||||
error: e => {
|
error: e => {
|
||||||
|
console.error('安装过程中出错:', e);
|
||||||
return getToastErrorMessage(e);
|
return getToastErrorMessage(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查应用安装权限失败:', error);
|
||||||
|
toast.error('无法验证安装权限,安装已取消');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAppInstall = (app, plan) => {
|
|
||||||
toast.promise(
|
|
||||||
this.$site.installApp.submit({
|
|
||||||
app: app.app,
|
|
||||||
plan: plan.name
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
loading: '正在创建安装任务...',
|
|
||||||
success: jobId => {
|
|
||||||
this.$emit('installed');
|
|
||||||
this.show = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (jobId) {
|
|
||||||
router.push({
|
|
||||||
name: 'Site Job',
|
|
||||||
params: {
|
|
||||||
name: this.site,
|
|
||||||
id: String(jobId)
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return '应用安装任务已创建';
|
|
||||||
},
|
|
||||||
error: e => {
|
|
||||||
return getToastErrorMessage(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: '应用',
|
label: '应用',
|
||||||
fieldname: 'app',
|
fieldname: 'app',
|
||||||
|
|||||||
@ -2410,3 +2410,80 @@ def get_site_config_standard_keys():
|
|||||||
["name", "key", "title", "description", "type"],
|
["name", "key", "title", "description", "type"],
|
||||||
order_by="title asc",
|
order_by="title asc",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@jingrow.whitelist()
|
||||||
|
def check_app_installable(name, app):
|
||||||
|
"""检查站点是否有权限安装指定的应用
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 站点名称
|
||||||
|
app: 应用名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含是否可安装的信息
|
||||||
|
{
|
||||||
|
"installable": True/False,
|
||||||
|
"required_plan_level": 1-5, # 如果不可安装
|
||||||
|
"current_plan_level": 1-5, # 当前站点计划等级
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"check_app_installable被调用: name={name}, app={app}")
|
||||||
|
|
||||||
|
site = jingrow.get_pg("Site", name)
|
||||||
|
print(f"获取到站点: {site.name}, plan={site.plan}")
|
||||||
|
|
||||||
|
# 获取应用的subscription_level
|
||||||
|
app_subscription_level = jingrow.db.get_value(
|
||||||
|
"Marketplace App", app, "subscription_level")
|
||||||
|
print(f"应用{app}的subscription_level={app_subscription_level}")
|
||||||
|
|
||||||
|
# 确保subscription_level是有效值
|
||||||
|
if app_subscription_level is None or app_subscription_level == "":
|
||||||
|
app_subscription_level = 1
|
||||||
|
print(f"应用{app}的subscription_level为空,使用默认值1")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
app_subscription_level = int(app_subscription_level)
|
||||||
|
print(f"应用{app}的subscription_level转换为整数: {app_subscription_level}")
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
print(f"转换subscription_level出错: {e}, 使用默认值1")
|
||||||
|
app_subscription_level = 1
|
||||||
|
|
||||||
|
# 获取站点的plan_level
|
||||||
|
site_plan_level = 1
|
||||||
|
if site.plan:
|
||||||
|
plan_level = jingrow.db.get_value(
|
||||||
|
"Site Plan", site.plan, "plan_level")
|
||||||
|
print(f"站点plan={site.plan}, plan_level={plan_level}")
|
||||||
|
|
||||||
|
if plan_level is not None and plan_level != "":
|
||||||
|
try:
|
||||||
|
site_plan_level = int(plan_level)
|
||||||
|
print(f"站点plan_level转换为整数: {site_plan_level}")
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
print(f"转换plan_level出错: {e}, 使用默认值1")
|
||||||
|
else:
|
||||||
|
print(f"站点没有plan信息,使用默认plan_level=1")
|
||||||
|
|
||||||
|
installable = site_plan_level >= app_subscription_level
|
||||||
|
print(f"安装条件判断: {site_plan_level} >= {app_subscription_level} = {installable}")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"installable": installable,
|
||||||
|
"required_plan_level": app_subscription_level,
|
||||||
|
"current_plan_level": site_plan_level
|
||||||
|
}
|
||||||
|
print(f"返回结果: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"check_app_installable出现异常: {e}")
|
||||||
|
# 发生异常时返回默认值,确保前端收到有效响应
|
||||||
|
return {
|
||||||
|
"installable": False,
|
||||||
|
"required_plan_level": 1,
|
||||||
|
"current_plan_level": 1,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
"allow_guest_to_view": 1,
|
"allow_guest_to_view": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2022-02-04 19:53:27.058972",
|
"creation": "2022-02-04 19:53:27.058972",
|
||||||
"pagetype": "PageType",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
@ -16,6 +15,7 @@
|
|||||||
"section_break_7",
|
"section_break_7",
|
||||||
"jingrow_approved",
|
"jingrow_approved",
|
||||||
"subscription_type",
|
"subscription_type",
|
||||||
|
"subscription_level",
|
||||||
"column_break_10",
|
"column_break_10",
|
||||||
"categories",
|
"categories",
|
||||||
"published",
|
"published",
|
||||||
@ -388,6 +388,15 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Localisation Apps",
|
"label": "Localisation Apps",
|
||||||
"options": "Marketplace Localisation App"
|
"options": "Marketplace Localisation App"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "subscription_level",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Subscription Level",
|
||||||
|
"options": "1\n2\n3\n4\n5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 1,
|
"has_web_view": 1,
|
||||||
@ -397,25 +406,26 @@
|
|||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"group": "General",
|
"group": "General",
|
||||||
"link_pagetype": "App Release Approval Request",
|
"link_fieldname": "marketplace_app",
|
||||||
"link_fieldname": "marketplace_app"
|
"link_pagetype": "App Release Approval Request"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "App Subscription",
|
"group": "App Subscription",
|
||||||
"link_pagetype": "Marketplace App Plan",
|
"link_fieldname": "app",
|
||||||
"link_fieldname": "app"
|
"link_pagetype": "Marketplace App Plan"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "App Subscription",
|
"group": "App Subscription",
|
||||||
"link_pagetype": "Marketplace App Subscription",
|
"link_fieldname": "app",
|
||||||
"link_fieldname": "app"
|
"link_pagetype": "Marketplace App Subscription"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-02-13 17:16:24.333531",
|
"modified": "2025-04-22 19:06:40.765966",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Jcloud",
|
"module": "Jcloud",
|
||||||
"name": "Marketplace App",
|
"name": "Marketplace App",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
"pagetype": "PageType",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
@ -430,6 +440,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [
|
"states": [
|
||||||
|
|||||||
@ -37,18 +37,11 @@ class MarketplaceApp(WebsiteGenerator):
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from jingrow.types import DF
|
from jcloud.jcloud.pagetype.marketplace_app_categories.marketplace_app_categories import MarketplaceAppCategories
|
||||||
|
from jcloud.jcloud.pagetype.marketplace_app_screenshot.marketplace_app_screenshot import MarketplaceAppScreenshot
|
||||||
from jcloud.jcloud.pagetype.marketplace_app_categories.marketplace_app_categories import (
|
|
||||||
MarketplaceAppCategories,
|
|
||||||
)
|
|
||||||
from jcloud.jcloud.pagetype.marketplace_app_screenshot.marketplace_app_screenshot import (
|
|
||||||
MarketplaceAppScreenshot,
|
|
||||||
)
|
|
||||||
from jcloud.jcloud.pagetype.marketplace_app_version.marketplace_app_version import MarketplaceAppVersion
|
from jcloud.jcloud.pagetype.marketplace_app_version.marketplace_app_version import MarketplaceAppVersion
|
||||||
from jcloud.jcloud.pagetype.marketplace_localisation_app.marketplace_localisation_app import (
|
from jcloud.jcloud.pagetype.marketplace_localisation_app.marketplace_localisation_app import MarketplaceLocalisationApp
|
||||||
MarketplaceLocalisationApp,
|
from jingrow.types import DF
|
||||||
)
|
|
||||||
|
|
||||||
after_install_script: DF.Code | None
|
after_install_script: DF.Code | None
|
||||||
after_uninstall_script: DF.Code | None
|
after_uninstall_script: DF.Code | None
|
||||||
@ -58,8 +51,8 @@ class MarketplaceApp(WebsiteGenerator):
|
|||||||
custom_verify_template: DF.Check
|
custom_verify_template: DF.Check
|
||||||
description: DF.SmallText
|
description: DF.SmallText
|
||||||
documentation: DF.Data | None
|
documentation: DF.Data | None
|
||||||
jingrow_approved: DF.Check
|
|
||||||
image: DF.AttachImage | None
|
image: DF.AttachImage | None
|
||||||
|
jingrow_approved: DF.Check
|
||||||
localisation_apps: DF.Table[MarketplaceLocalisationApp]
|
localisation_apps: DF.Table[MarketplaceLocalisationApp]
|
||||||
long_description: DF.TextEditor | None
|
long_description: DF.TextEditor | None
|
||||||
message: DF.TextEditor | None
|
message: DF.TextEditor | None
|
||||||
@ -68,15 +61,7 @@ class MarketplaceApp(WebsiteGenerator):
|
|||||||
poll_method: DF.Data | None
|
poll_method: DF.Data | None
|
||||||
privacy_policy: DF.Data | None
|
privacy_policy: DF.Data | None
|
||||||
published: DF.Check
|
published: DF.Check
|
||||||
review_stage: DF.Literal[
|
review_stage: DF.Literal["Not Started", "Description Missing", "Logo Missing", "App Release Not Reviewed", "Ready for Review", "Ready to Publish", "Rejected"]
|
||||||
"Not Started",
|
|
||||||
"Description Missing",
|
|
||||||
"Logo Missing",
|
|
||||||
"App Release Not Reviewed",
|
|
||||||
"Ready for Review",
|
|
||||||
"Ready to Publish",
|
|
||||||
"Rejected",
|
|
||||||
]
|
|
||||||
route: DF.Data | None
|
route: DF.Data | None
|
||||||
run_after_install_script: DF.Check
|
run_after_install_script: DF.Check
|
||||||
run_after_uninstall_script: DF.Check
|
run_after_uninstall_script: DF.Check
|
||||||
@ -88,6 +73,7 @@ class MarketplaceApp(WebsiteGenerator):
|
|||||||
status: DF.Literal["Draft", "Published", "In Review", "Attention Required", "Rejected", "Disabled"]
|
status: DF.Literal["Draft", "Published", "In Review", "Attention Required", "Rejected", "Disabled"]
|
||||||
stop_auto_review: DF.Check
|
stop_auto_review: DF.Check
|
||||||
subject: DF.Data | None
|
subject: DF.Data | None
|
||||||
|
subscription_level: DF.Literal["1", "2", "3", "4", "5"]
|
||||||
subscription_type: DF.Literal["Free", "Paid", "Freemium"]
|
subscription_type: DF.Literal["Free", "Paid", "Freemium"]
|
||||||
subscription_update_hook: DF.Data | None
|
subscription_update_hook: DF.Data | None
|
||||||
support: DF.Data | None
|
support: DF.Data | None
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"column_break_5",
|
"column_break_5",
|
||||||
"price_cny",
|
"price_cny",
|
||||||
"price_usd",
|
"price_usd",
|
||||||
|
"plan_level",
|
||||||
"allow_downgrading_from_other_plan",
|
"allow_downgrading_from_other_plan",
|
||||||
"features_section",
|
"features_section",
|
||||||
"cpu_time_per_day",
|
"cpu_time_per_day",
|
||||||
@ -235,11 +236,18 @@
|
|||||||
"fieldname": "allow_downgrading_from_other_plan",
|
"fieldname": "allow_downgrading_from_other_plan",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Downgrading From Other Plan"
|
"label": "Allow Downgrading From Other Plan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "plan_level",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Plan Level",
|
||||||
|
"options": "1\n2\n3\n4\n5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-04-20 20:46:00.570720",
|
"modified": "2025-04-22 18:40:33.565094",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Jcloud",
|
"module": "Jcloud",
|
||||||
"name": "Site Plan",
|
"name": "Site Plan",
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class SitePlan(Plan):
|
|||||||
memory: DF.Int
|
memory: DF.Int
|
||||||
monitor_access: DF.Check
|
monitor_access: DF.Check
|
||||||
offsite_backups: DF.Check
|
offsite_backups: DF.Check
|
||||||
|
plan_level: DF.Literal["1", "2", "3", "4", "5"]
|
||||||
plan_title: DF.Data | None
|
plan_title: DF.Data | None
|
||||||
price_cny: DF.Currency
|
price_cny: DF.Currency
|
||||||
price_usd: DF.Currency
|
price_usd: DF.Currency
|
||||||
@ -52,6 +53,7 @@ class SitePlan(Plan):
|
|||||||
dashboard_fields = (
|
dashboard_fields = (
|
||||||
"name",
|
"name",
|
||||||
"plan_title",
|
"plan_title",
|
||||||
|
"plan_level",
|
||||||
"interval",
|
"interval",
|
||||||
"document_type",
|
"document_type",
|
||||||
"document_name",
|
"document_name",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user