add bulk actions support
37
README.rst
@@ -173,10 +173,43 @@ Search Fields
|
||||
~~~~~~~~~~~~~
|
||||
Defined ``menu.search_fields`` in ``menu`` will render a search form by fields.
|
||||
|
||||
Excel Export
|
||||
~~~~~~~~~~~~
|
||||
Xlsx Export
|
||||
~~~~~~~~~~~
|
||||
FastAPI-admin can export searched data to excel file when define ``{export : True}`` in ``menu.actions``.
|
||||
|
||||
Bulk Actions
|
||||
~~~~~~~~~~~~
|
||||
Current FastAPI-admin support builtin bulk action ``delete_all``,if you want write your own bulk actions:
|
||||
|
||||
1. pass ``bulk_actions`` in ``Menu``,example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Menu(
|
||||
...
|
||||
bulk_actions=[{
|
||||
'value': 'delete', # this is fastapi router path param.
|
||||
'text': 'delete_all', # this will show in front.
|
||||
}]
|
||||
)
|
||||
|
||||
2. write fastapi route,example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_admin.schemas import BulkIn
|
||||
from fastapi_admin.factory import app as admin_app
|
||||
|
||||
@admin_app.post(
|
||||
'/{resource}/bulk/delete' # delete is defined before.
|
||||
)
|
||||
async def bulk_delete(
|
||||
bulk_in: BulkIn,
|
||||
model=Depends(get_model)
|
||||
):
|
||||
await model.filter(pk__in=bulk_in.pk_list).delete()
|
||||
return {'success': True}
|
||||
|
||||
Deployment
|
||||
==========
|
||||
1. Deploy fastapi app by gunicorn+uvicorn or reference https://fastapi.tiangolo.com/deployment/.
|
||||
|
||||
@@ -50,7 +50,7 @@ def create_app():
|
||||
url='/rest/App',
|
||||
icon='fa fa-pencil',
|
||||
sort_fields=('uaid',),
|
||||
search_fields=('uaid',)
|
||||
search_fields=('uaid',),
|
||||
),
|
||||
Menu(
|
||||
name='多对多测试',
|
||||
@@ -97,7 +97,8 @@ def create_app():
|
||||
Menu(
|
||||
name='请求日志',
|
||||
url='/rest/ApiLog',
|
||||
icon='fa fa-sticky-note'
|
||||
icon='fa fa-sticky-note',
|
||||
search_fields=('app',),
|
||||
),
|
||||
Menu(
|
||||
name='App版本',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from . import routes
|
||||
|
||||
__version__ = '0.1.7'
|
||||
__version__ = '0.1.8'
|
||||
|
||||
@@ -27,7 +27,7 @@ class QueryItem(BaseModel):
|
||||
page: int = 1
|
||||
sort: dict
|
||||
where: dict
|
||||
with_: dict
|
||||
with_: dict = {}
|
||||
size: int = 10
|
||||
sort: dict = {}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import importlib
|
||||
from typing import Type
|
||||
from typing import Type, List, Dict
|
||||
|
||||
from fastapi import FastAPI
|
||||
from tortoise import Model, Tortoise
|
||||
@@ -26,7 +26,7 @@ class AdminApp(FastAPI):
|
||||
'SmallIntField': 'number',
|
||||
'JSONField': 'json',
|
||||
}
|
||||
model_menu_mapping = {}
|
||||
model_menu_mapping: Dict[str, Menu] = {}
|
||||
|
||||
def _get_model_menu_mapping(self):
|
||||
for menu in filter(lambda x: x.url, self.site.menus):
|
||||
@@ -179,7 +179,8 @@ class AdminApp(FastAPI):
|
||||
title=model_describe.get('description') or resource.title(),
|
||||
fields=fields,
|
||||
searchFields=search_fields,
|
||||
pk=pk
|
||||
pk=pk,
|
||||
bulk_actions=self.model_menu_mapping[resource].bulk_actions,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..common import handle_m2m_fields_create_or_update
|
||||
from ..depends import QueryItem, get_query, parse_body, get_model
|
||||
from ..factory import app
|
||||
from ..responses import GetManyOut
|
||||
from ..schemas import BulkIn
|
||||
from ..shortcuts import get_object_or_404
|
||||
|
||||
router = APIRouter()
|
||||
@@ -73,6 +74,17 @@ async def view(
|
||||
return resource.dict(by_alias=True, exclude_unset=True)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/{resource}/bulk/delete'
|
||||
)
|
||||
async def bulk_delete(
|
||||
bulk_in: BulkIn,
|
||||
model=Depends(get_model)
|
||||
):
|
||||
await model.filter(pk__in=bulk_in.pk_list).delete()
|
||||
return {'success': True}
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{resource}/{id}'
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import Body
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -5,3 +7,7 @@ from pydantic import BaseModel
|
||||
class LoginIn(BaseModel):
|
||||
username: str = Body(..., example='long2ice')
|
||||
password: str = Body(..., example='123456')
|
||||
|
||||
|
||||
class BulkIn(BaseModel):
|
||||
pk_list: List = Body(..., example=[1, 2, 3])
|
||||
|
||||
@@ -23,11 +23,12 @@ class Menu(BaseModel):
|
||||
# define field type,like select,radiolist,text,date
|
||||
fields_type: Dict = {}
|
||||
actions: Dict = {
|
||||
'toolbar': {
|
||||
'delete_all': True
|
||||
},
|
||||
'export': True
|
||||
}
|
||||
bulk_actions: List[Dict] = [{
|
||||
'value': 'delete',
|
||||
'text': 'delete_all',
|
||||
}]
|
||||
|
||||
|
||||
class Site(BaseModel):
|
||||
@@ -72,6 +73,7 @@ class Resource(BaseModel):
|
||||
pk: str
|
||||
resource_fields: Dict[str, Union[Field, Dict]]
|
||||
searchFields: Optional[Dict[str, Field]]
|
||||
bulk_actions: Optional[List[Dict]]
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"actions": {
|
||||
|
||||
"select_all": "Select all",
|
||||
"clear_selected": "Clear selected",
|
||||
"delete_all": "Delete all"
|
||||
},
|
||||
"messages": {
|
||||
"paginate": "Total: {total}",
|
||||
@@ -9,10 +11,10 @@
|
||||
"deleted_all": "All deleted",
|
||||
"confirm_delete": "Are you sure you want to delete?",
|
||||
"image_size": "Size limit: {width}x{height}",
|
||||
"confirm_delete_all": "Are you sure you want to delete all data?",
|
||||
"no_options_text": "No Options Here!"
|
||||
"confirm_bulk_action": "Are you sure you want to do bulk action?",
|
||||
"no_options_text": "No Options Here!",
|
||||
"bulk_success": "Bulk action success!"
|
||||
},
|
||||
"errors": {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
"change": "更换",
|
||||
"choose": "选择",
|
||||
"login": "登录",
|
||||
"edit": "编辑"
|
||||
"edit": "编辑",
|
||||
"select_all": "选择全部",
|
||||
"clear_selected": "清除选择"
|
||||
},
|
||||
"texts": {
|
||||
|
||||
},
|
||||
"fields": {
|
||||
"username": "用户名",
|
||||
@@ -38,8 +39,9 @@
|
||||
"deleted_all": "全部删除成功",
|
||||
"confirm_delete": "确定要删除吗?",
|
||||
"image_size": "尺寸要求:{width}x{height}",
|
||||
"confirm_delete_all": "此次操作不可恢复,确定要全部删除吗?",
|
||||
"no_options_text": "暂无可选项"
|
||||
"confirm_bulk_action": "确定执行批量操作?",
|
||||
"no_options_text": "暂无可选项",
|
||||
"bulk_success": "批量操作成功!"
|
||||
},
|
||||
"errors": {
|
||||
"too_large": "请上传小于{limit}KB的文件",
|
||||
|
||||
@@ -1,62 +1,71 @@
|
||||
/**
|
||||
* See https://bootswatch.com/
|
||||
* Change `lumen` to any other words blow:
|
||||
*
|
||||
*
|
||||
* cerulean darkly litera materia sandstone slate superhero
|
||||
* cosmo flatly lumen minty simplex solar united
|
||||
* cyborg journal lux pulse sketchy spacelab yeti
|
||||
*/
|
||||
|
||||
|
||||
// @import "node_modules/bootswatch/dist/lumen/bootstrap";
|
||||
|
||||
.app-footer{
|
||||
border-top: none;
|
||||
color: #666;
|
||||
}
|
||||
.card {
|
||||
.app-footer {
|
||||
border-top: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 0 5px #ccc;
|
||||
}
|
||||
.card-header{
|
||||
border:none;
|
||||
|
||||
.card-header {
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
.card-footer{
|
||||
|
||||
.card-footer {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
border-color:transparent !important;
|
||||
border-color: transparent !important;
|
||||
border-radius: 4px;
|
||||
margin: 0 0.2em;
|
||||
padding: 0.3em 1em !important;
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
.data-form .tabs .nav-tabs {
|
||||
margin: 0 1em 1em 1em;
|
||||
border:none;
|
||||
}
|
||||
.nav-tabs .nav-link.active{
|
||||
border-color:#ccc !important;
|
||||
border-radius: 2em;
|
||||
|
||||
}
|
||||
.tab-content{
|
||||
border-color:transparent;
|
||||
|
||||
}
|
||||
|
||||
.btn{
|
||||
.data-form .tabs .nav-tabs {
|
||||
margin: 0 1em 1em 1em;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
border-color: #ccc !important;
|
||||
border-radius: 2em;
|
||||
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5em 1.2em;
|
||||
// border-radius: 2em;
|
||||
//border-radius: 2em;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
border-top: 1px solid #dee2e65c;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
@@ -30,15 +30,6 @@
|
||||
v-bind="button"
|
||||
>{{button.label}}
|
||||
</b-btn>
|
||||
<b-btn
|
||||
@click="removeAll"
|
||||
class="pull-right"
|
||||
variant="second"
|
||||
v-if="_.get(actions, 'toolbar.delete_all') === true"
|
||||
>
|
||||
<i class="icon-trash"></i>
|
||||
{{$t('actions.delete_all')}}
|
||||
</b-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class>
|
||||
@@ -88,6 +79,9 @@
|
||||
ref="table"
|
||||
:items="fetchItems"
|
||||
:fields="columns"
|
||||
selectable
|
||||
@row-selected="onRowSelected"
|
||||
:select-mode="selectModel"
|
||||
:current-page="currentPage"
|
||||
:sort-by.sync="sortBy"
|
||||
:sort-desc.sync="sortDesc"
|
||||
@@ -140,7 +134,12 @@
|
||||
</b-btn>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<div class="form-inline my-2">
|
||||
<b-button class="mr-1" @click="selectAllRows">{{ $t("actions.select_all") }}</b-button>
|
||||
<b-button class="mr-1" @click="clearSelected">{{ $t("actions.clear_selected")}}</b-button>
|
||||
<b-form-select class="mr-1" v-model="selectBulkAction" :options="bulkActions"></b-form-select>
|
||||
<b-button @click="submitBulk" variant="primary">{{ $t("actions.submit")}}</b-button>
|
||||
</div>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-10">
|
||||
<b-pagination
|
||||
@@ -170,6 +169,8 @@
|
||||
init: false,
|
||||
loaded: false,
|
||||
table: {},
|
||||
modes: ['multi', 'single', 'range'],
|
||||
selectModel: 'range',
|
||||
total: 0, //total rows
|
||||
pageLimit: 10, //display how many page buttons
|
||||
currentPage: 1,
|
||||
@@ -179,6 +180,9 @@
|
||||
perPage: 10,
|
||||
where: {},
|
||||
pk: null,
|
||||
selected_pk_list: [],
|
||||
bulkActions: {},
|
||||
selectBulkAction: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -221,10 +225,30 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitBulk() {
|
||||
if (window.confirm(this.$t("messages.confirm_bulk_action"))) {
|
||||
this.$http.post(this.uri + '/bulk/' + this.selectBulkAction, {
|
||||
pk_list: this.selected_pk_list
|
||||
}).then(() => {
|
||||
this.$snotify.success(this.$t("messages.bulk_success"));
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
},
|
||||
selectAllRows() {
|
||||
this.$refs.table.selectAllRows()
|
||||
},
|
||||
onRowSelected(items) {
|
||||
this.selected_pk_list = _.map(items, item => {
|
||||
return item[this.pk];
|
||||
})
|
||||
},
|
||||
clearSelected() {
|
||||
this.$refs.table.clearSelected()
|
||||
},
|
||||
doSearch(params) {
|
||||
this.where = _.omitBy(params, v => v === null);
|
||||
this.$refs.table.refresh();
|
||||
// console.log(params);
|
||||
},
|
||||
searchAndExport() {
|
||||
const query = JSON.stringify({
|
||||
@@ -300,6 +324,12 @@
|
||||
fetch() {
|
||||
this.init = false;
|
||||
this.$http.get(this.uri + "/grid").then(res => {
|
||||
_.mapValues(res.data.bulk_actions, action => {
|
||||
action.text = this.$t(`actions.${action.text}`)
|
||||
});
|
||||
|
||||
this.bulkActions = res.data.bulk_actions;
|
||||
|
||||
_.mapValues(res.data.fields, field => {
|
||||
field.thClass = "bg-light";
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 271 KiB |
BIN
images/list.png
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 389 KiB |
BIN
images/login.png
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 168 KiB |
BIN
images/view.png
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 280 KiB |