Skip to content

Commit da1eb65

Browse files
authored
feat: allow copy config from existing configs (#6785)
* feat: allow copy config from existing configs * fix: issues mentioned by reviewer bot - duplicated logic for initializing/resetting the config - check whitespace-only names
1 parent bbec8ef commit da1eb65

File tree

4 files changed

+124
-31
lines changed

4 files changed

+124
-31
lines changed

dashboard/src/i18n/locales/en-US/features/config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,16 @@
7777
"title": "Configuration Management",
7878
"description": "AstrBot supports separate configuration files for different bots. The `default` configuration is used by default.",
7979
"newConfig": "New Configuration",
80+
"copyConfig": "Copy Configuration",
8081
"editConfig": "Edit Configuration",
8182
"manageConfigs": "Manage Configurations...",
8283
"configName": "Name",
8384
"fillConfigName": "Enter configuration name",
8485
"confirmDelete": "Are you sure you want to delete the configuration \"{name}\"? This action cannot be undone.",
8586
"pleaseEnterName": "Please enter a configuration name",
87+
"nameExists": "Configuration name already exists. Please use another name.",
8688
"createFailed": "Failed to create new configuration",
89+
"copyFailed": "Failed to copy configuration",
8790
"deleteFailed": "Failed to delete configuration",
8891
"updateFailed": "Failed to update configuration"
8992
},

dashboard/src/i18n/locales/ru-RU/features/config.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,16 @@
7777
"title": "Управление конфигурациями",
7878
"description": "AstrBot поддерживает несколько конфигураций для разных ботов. По умолчанию используется «default».",
7979
"newConfig": "Новая конфигурация",
80+
"copyConfig": "Копировать конфигурацию",
8081
"editConfig": "Изменить конфигурацию",
8182
"manageConfigs": "Управление файлами...",
8283
"configName": "Имя",
8384
"fillConfigName": "Введите имя конфигурации",
8485
"confirmDelete": "Вы уверены, что хотите удалить конфигурацию «{name}»? Это действие необратимо.",
8586
"pleaseEnterName": "Пожалуйста, введите имя",
87+
"nameExists": "Имя конфигурации уже существует. Используйте другое имя.",
8688
"createFailed": "Ошибка создания конфигурации",
89+
"copyFailed": "Ошибка копирования конфигурации",
8790
"deleteFailed": "Ошибка удаления",
8891
"updateFailed": "Ошибка обновления"
8992
},
@@ -126,4 +129,4 @@
126129
"cancel": "Отмена"
127130
}
128131
}
129-
}
132+
}

dashboard/src/i18n/locales/zh-CN/features/config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,16 @@
7777
"title": "配置文件管理",
7878
"description": "AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。",
7979
"newConfig": "新建配置文件",
80+
"copyConfig": "复制配置文件",
8081
"editConfig": "编辑配置文件",
8182
"manageConfigs": "管理配置文件...",
8283
"configName": "名称",
8384
"fillConfigName": "填写配置文件名称",
8485
"confirmDelete": "确定要删除配置文件 \"{name}\" 吗?此操作不可恢复。",
8586
"pleaseEnterName": "请填写配置名称",
87+
"nameExists": "配置文件名称已存在,请使用其他名称",
8688
"createFailed": "新配置文件创建失败",
89+
"copyFailed": "复制配置文件失败",
8790
"deleteFailed": "删除配置文件失败",
8891
"updateFailed": "更新配置文件失败"
8992
},

dashboard/src/views/ConfigPage.vue

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,15 @@
126126
<!-- Config List -->
127127
<v-list lines="two">
128128
<v-list-item v-for="config in configInfoList" :key="config.id" :title="config.name">
129-
<template v-slot:append v-if="config.id !== 'default'">
129+
<template v-slot:append>
130130
<div class="d-flex align-center" style="gap: 8px;">
131+
<v-btn icon="mdi-content-copy" size="small" variant="text" color="primary"
132+
@click="startCopyConfig(config)"></v-btn>
131133
<v-btn icon="mdi-pencil" size="small" variant="text" color="warning"
134+
v-if="config.id !== 'default'"
132135
@click="startEditConfig(config)"></v-btn>
133136
<v-btn icon="mdi-delete" size="small" variant="text" color="error"
137+
v-if="config.id !== 'default'"
134138
@click="confirmDeleteConfig(config)"></v-btn>
135139
</div>
136140
</template>
@@ -141,7 +145,7 @@
141145
<v-divider v-if="showConfigForm" class="my-6"></v-divider>
142146

143147
<div v-if="showConfigForm">
144-
<h3 class="mb-4">{{ isEditingConfig ? tm('configManagement.editConfig') : tm('configManagement.newConfig') }}</h3>
148+
<h3 class="mb-4">{{ configFormTitle }}</h3>
145149

146150
<h4>{{ tm('configManagement.configName') }}</h4>
147151

@@ -151,7 +155,7 @@
151155
<div class="d-flex justify-end mt-4" style="gap: 8px;">
152156
<v-btn variant="text" @click="cancelConfigForm">{{ tm('buttons.cancel') }}</v-btn>
153157
<v-btn color="primary" @click="saveConfigForm"
154-
:disabled="!configFormData.name">
158+
:disabled="isConfigFormSaveDisabled">
155159
{{ isEditingConfig ? tm('buttons.update') : tm('buttons.create') }}
156160
</v-btn>
157161
</div>
@@ -297,6 +301,19 @@ export default {
297301
selectedConfigInfo() {
298302
return this.configInfoList.find(info => info.id === this.selectedConfigID) || {};
299303
},
304+
configFormTitle() {
305+
if (this.isEditingConfig) {
306+
return this.tm('configManagement.editConfig');
307+
}
308+
if (this.isCopyingConfig) {
309+
return this.tm('configManagement.copyConfig');
310+
}
311+
return this.tm('configManagement.newConfig');
312+
},
313+
isConfigFormSaveDisabled() {
314+
const isNameEmpty = !this.normalizeConfigName(this.configFormData.name);
315+
return isNameEmpty || (this.isCopyingConfig && !this.copySourceConfigId);
316+
},
300317
configSelectItems() {
301318
const items = [...this.configInfoList];
302319
items.push({
@@ -343,6 +360,7 @@ export default {
343360
configManageDialog: false,
344361
showConfigForm: false,
345362
isEditingConfig: false,
363+
isCopyingConfig: false,
346364
config_data_has_changed: false,
347365
config_data_str: "",
348366
config_data: {
@@ -371,6 +389,7 @@ export default {
371389
name: '',
372390
},
373391
editingConfigId: null,
392+
copySourceConfigId: '',
374393
375394
// 测试聊天
376395
testChatDrawer: false,
@@ -567,9 +586,9 @@ export default {
567586
this.save_message_snack = true;
568587
}
569588
},
570-
createNewConfig() {
589+
createNewConfig(configName) {
571590
axios.post('/api/config/abconf/new', {
572-
name: this.configFormData.name
591+
name: configName
573592
}).then((res) => {
574593
if (res.data.status === "ok") {
575594
this.save_message = res.data.message;
@@ -589,6 +608,24 @@ export default {
589608
this.save_message_success = "error";
590609
});
591610
},
611+
normalizeConfigName(name) {
612+
return typeof name === 'string' ? name.trim() : '';
613+
},
614+
hasDuplicateConfigName(name, excludeId = null) {
615+
const normalizedName = this.normalizeConfigName(name);
616+
if (!normalizedName) {
617+
return false;
618+
}
619+
return this.configInfoList.some((config) => {
620+
if (!config || !config.name) {
621+
return false;
622+
}
623+
if (excludeId && config.id === excludeId) {
624+
return false;
625+
}
626+
return this.normalizeConfigName(config.name) === normalizedName;
627+
});
628+
},
592629
async onConfigSelect(value) {
593630
if (value === '_%manage%_') {
594631
this.configManageDialog = true;
@@ -638,45 +675,92 @@ export default {
638675
}
639676
}
640677
},
678+
setConfigFormState({ mode = 'create', config = null, visible = true } = {}) {
679+
this.showConfigForm = visible;
680+
this.isEditingConfig = mode === 'edit';
681+
this.isCopyingConfig = mode === 'copy';
682+
this.editingConfigId = this.isEditingConfig && config ? config.id : null;
683+
this.copySourceConfigId = this.isCopyingConfig && config ? config.id : '';
684+
685+
let name = '';
686+
if (this.isEditingConfig && config) {
687+
name = config.name || '';
688+
} else if (this.isCopyingConfig && config) {
689+
name = `${config.name || ''}-copy`;
690+
}
691+
this.configFormData = { name };
692+
},
641693
startCreateConfig() {
642-
this.showConfigForm = true;
643-
this.isEditingConfig = false;
644-
this.configFormData = {
645-
name: '',
646-
};
647-
this.editingConfigId = null;
694+
this.setConfigFormState({ mode: 'create' });
648695
},
649696
startEditConfig(config) {
650-
this.showConfigForm = true;
651-
this.isEditingConfig = true;
652-
this.editingConfigId = config.id;
653-
654-
this.configFormData = {
655-
name: config.name || '',
656-
};
697+
this.setConfigFormState({ mode: 'edit', config });
698+
},
699+
startCopyConfig(config) {
700+
this.setConfigFormState({ mode: 'copy', config });
657701
},
658702
cancelConfigForm() {
659-
this.showConfigForm = false;
660-
this.isEditingConfig = false;
661-
this.editingConfigId = null;
662-
this.configFormData = {
663-
name: '',
664-
};
703+
this.setConfigFormState({ visible: false });
665704
},
666705
saveConfigForm() {
667-
if (!this.configFormData.name) {
706+
const normalizedName = this.normalizeConfigName(this.configFormData.name);
707+
if (!normalizedName) {
668708
this.save_message = this.tm('configManagement.pleaseEnterName');
669709
this.save_message_snack = true;
670710
this.save_message_success = "error";
671711
return;
672712
}
673-
713+
const excludeId = this.isEditingConfig ? this.editingConfigId : null;
714+
if (this.hasDuplicateConfigName(normalizedName, excludeId)) {
715+
this.save_message = this.tm('configManagement.nameExists');
716+
this.save_message_snack = true;
717+
this.save_message_success = "error";
718+
return;
719+
}
720+
this.configFormData.name = normalizedName;
674721
if (this.isEditingConfig) {
675-
this.updateConfigInfo();
722+
this.updateConfigInfo(normalizedName);
723+
} else if (this.isCopyingConfig) {
724+
this.copyConfig(normalizedName);
676725
} else {
677-
this.createNewConfig();
726+
this.createNewConfig(normalizedName);
678727
}
679728
},
729+
copyConfig(configName) {
730+
axios.get('/api/config/abconf', {
731+
params: { id: this.copySourceConfigId }
732+
}).then((res) => {
733+
const sourceConfig = res.data?.data?.config;
734+
if (!sourceConfig) {
735+
this.save_message = this.tm('configManagement.copyFailed');
736+
this.save_message_snack = true;
737+
this.save_message_success = "error";
738+
return;
739+
}
740+
return axios.post('/api/config/abconf/new', {
741+
name: configName,
742+
config: sourceConfig
743+
});
744+
}).then((res) => {
745+
if (!res) return;
746+
if (res.data.status === "ok") {
747+
this.save_message = res.data.message;
748+
this.save_message_snack = true;
749+
this.save_message_success = "success";
750+
this.getConfigInfoList(res.data.data.conf_id);
751+
this.cancelConfigForm();
752+
} else {
753+
this.save_message = res.data.message;
754+
this.save_message_snack = true;
755+
this.save_message_success = "error";
756+
}
757+
}).catch((err) => {
758+
console.error(err);
759+
this.save_message = err?.response?.data?.message || this.tm('configManagement.copyFailed');
760+
this.save_message_snack = true;
761+
this.save_message_success = "error";
762+
});
763+
},
680764
async confirmDeleteConfig(config) {
681765
const message = this.tm('configManagement.confirmDelete').replace('{name}', config.name);
682766
if (await askForConfirmationDialog(message, this.confirmDialog)) {
@@ -706,10 +790,10 @@ export default {
706790
this.save_message_success = "error";
707791
});
708792
},
709-
updateConfigInfo() {
793+
updateConfigInfo(configName) {
710794
axios.post('/api/config/abconf/update', {
711795
id: this.editingConfigId,
712-
name: this.configFormData.name
796+
name: configName
713797
}).then((res) => {
714798
if (res.data.status === "ok") {
715799
this.save_message = res.data.message;

0 commit comments

Comments
 (0)