Merge pull request #2810 from m7modSy/develop

Reorder books #2338
This commit is contained in:
vabene1111 2024-02-16 19:53:38 +01:00 committed by GitHub
commit 631af65cf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 205 additions and 42 deletions

View File

@ -983,6 +983,8 @@ class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionMod
shared = models.ManyToManyField(User, blank=True, related_name='shared_with') shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_by = models.ForeignKey(User, on_delete=models.CASCADE)
filter = models.ForeignKey('cookbook.CustomFilter', null=True, blank=True, on_delete=models.SET_NULL) filter = models.ForeignKey('cookbook.CustomFilter', null=True, blank=True, on_delete=models.SET_NULL)
order = models.IntegerField(default=0)
space = models.ForeignKey(Space, on_delete=models.CASCADE) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space') objects = ScopedManager(space='space')

View File

@ -979,7 +979,7 @@ class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer)
class Meta: class Meta:
model = RecipeBook model = RecipeBook
fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter') fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter', 'order')
read_only_fields = ('created_by',) read_only_fields = ('created_by',)

View File

@ -662,8 +662,16 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
def get_queryset(self): def get_queryset(self):
order_field = self.request.GET.get('order_field')
order_direction = self.request.GET.get('order_direction')
if not order_field:
order_field = 'id'
ordering = f"{'' if order_direction == 'asc' else '-'}{order_field}"
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter( self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(
space=self.request.space).distinct() space=self.request.space).distinct().order_by(ordering)
return super().get_queryset() return super().get_queryset()

View File

@ -11,50 +11,90 @@
<b-button variant="primary" v-b-tooltip.hover :title="$t('Create')" @click="createNew"> <b-button variant="primary" v-b-tooltip.hover :title="$t('Create')" @click="createNew">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</b-button> </b-button>
<b-dropdown variant="primary" id="sortDropDown" text="Order By" class="border-left">
<b-dropdown-item @click = "orderBy('id','asc')" :disabled= "isActiveSort('id','asc')">oldest to newest</b-dropdown-item>
<b-dropdown-item @click = "orderBy('id','desc')" :disabled= "isActiveSort('id','desc')">newest to oldest</b-dropdown-item>
<b-dropdown-item @click = "orderBy('name','asc')" :disabled= "isActiveSort('name','asc')">alphabetical order</b-dropdown-item>
<b-dropdown-item @click = "orderBy('order','asc')" :disabled= "isActiveSort('order','asc')" >manually</b-dropdown-item>
</b-dropdown>
</b-input-group-append> </b-input-group-append>
<b-button class= "ml-2" variant="primary" v-if="isActiveSort('order','asc')" @click="handleEditButton" >
{{submitText}}
</b-button>
</b-input-group> </b-input-group>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div style="padding-bottom: 55px"> <div v-if="!isActiveSort('order','asc') || !manSubmitted">
<div class="mb-3" v-for="book in filteredBooks" :key="book.id"> <div style="padding-bottom: 55px">
<div class="row"> <div class="mb-3" v-for="(book) in filteredBooks" :key="book.id">
<div class="col-md-12"> <div class="row">
<b-card class="d-flex flex-column" v-hover v-on:click="openBook(book.id)"> <div class="col-md-12">
<b-row no-gutters style="height: inherit"> <b-card class="d-flex flex-column" v-hover >
<b-col no-gutters md="10" style="height: inherit"> <b-row no-gutters style="height: inherit" class="d-flex align-items-center">
<b-card-body class="m-0 py-0" style="height: inherit"> <b-col no-gutters style="height: inherit">
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis"> <b-card-body class="m-0 py-0" style="height: inherit">
<h5 class="m-0 mt-1 text-truncate"> <b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
{{ book.name }} <span class="float-right"><i class="fa fa-book"></i></span> <b-button v-on:click="openBook(book.id)" style="color: #000; background-color: white" variant="primary">
</h5> <h5 class="m-0 mt-1 text-truncate" >
<div class="m-0 text-truncate">{{ book.description }}</div> {{ book.name }} <span class="float-right"><i class="fa fa-book"></i></span>
<div class="mt-auto mb-1 d-flex flex-row justify-content-end"></div> </h5></b-button>
</b-card-text> <div class="m-0 text-truncate">{{ book.description }}</div>
</b-card-body> <div class="mt-auto mb-1 d-flex flex-row justify-content-end"></div>
</b-col> </b-card-text>
</b-row> </b-card-body>
</b-card> </b-col>
</b-row>
</b-card>
</div>
</div>
<loading-spinner v-if="current_book === book.id && loading"></loading-spinner>
<transition name="slide-fade">
<cookbook-slider
:recipes="recipes"
:book="book"
:key="`slider_${book.id}`"
v-if="current_book === book.id && !loading"
v-on:refresh="refreshData"
@reload="openBook(current_book, true)"
></cookbook-slider>
</transition>
</div> </div>
</div> </div>
</div>
<loading-spinner v-if="current_book === book.id && loading"></loading-spinner> <div v-else>
<transition name="slide-fade"> <draggable
<cookbook-slider @change="updateManualSorting"
:recipes="recipes" :list="cookbooks" ghost-class="ghost">
:book="book" <b-card no-body class="mt-1 list-group-item p-2"
:key="`slider_${book.id}`" style="cursor: move"
v-if="current_book === book.id && !loading" v-for=" (book,index) in filteredBooks"
v-on:refresh="refreshData" v-hover
@reload="openBook(current_book, true)" :key="book.id">
></cookbook-slider> <b-card-header class="p-2 border-0">
</transition> <div class="row">
<div class="col-2">
<button type="button"
class="btn btn-lg shadow-none"><i
class="fas fa-arrows-alt-v"></i></button>
</div>
<div class="col-10">
<h5 class="mt-1 mb-1">
<b-badge class="float-left text-white mr-2">
#{{ index + 1 }}
</b-badge>
{{ book.name }}
</h5>
</div>
</div>
</b-card-header>
</b-card>
</draggable>
</div> </div>
</div>
<bottom-navigation-bar active-view="view_books"> <bottom-navigation-bar active-view="view_books">
<template #custom_create_functions> <template #custom_create_functions>
@ -72,7 +112,7 @@
<script> <script>
import Vue from "vue" import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue" import { BootstrapVue } from "bootstrap-vue"
import draggable from "vuedraggable"
import "bootstrap-vue/dist/bootstrap-vue.css" import "bootstrap-vue/dist/bootstrap-vue.css"
import { ApiApiFactory } from "@/utils/openapi/api" import { ApiApiFactory } from "@/utils/openapi/api"
import CookbookSlider from "@/components/CookbookSlider" import CookbookSlider from "@/components/CookbookSlider"
@ -85,7 +125,7 @@ Vue.use(BootstrapVue)
export default { export default {
name: "CookbookView", name: "CookbookView",
mixins: [ApiMixin], mixins: [ApiMixin],
components: { LoadingSpinner, CookbookSlider, BottomNavigationBar }, components: { LoadingSpinner, CookbookSlider, BottomNavigationBar, draggable },
data() { data() {
return { return {
cookbooks: [], cookbooks: [],
@ -94,6 +134,11 @@ export default {
current_book: undefined, current_book: undefined,
loading: false, loading: false,
search: "", search: "",
activeSortField : 'id',
activeSortDirection: 'desc',
inputValue: "",
manSubmitted : false,
submitText: "Edit"
} }
}, },
computed: { computed: {
@ -110,7 +155,7 @@ export default {
methods: { methods: {
refreshData: function () { refreshData: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.listRecipeBooks().then((result) => { apiClient.listRecipeBooks().then((result) => {
this.cookbooks = result.data this.cookbooks = result.data
}) })
@ -168,7 +213,45 @@ export default {
} }
}) })
}, },
}, orderBy: function(order_field,order_direction){
let apiClient = new ApiApiFactory()
const options = {
order_field: order_field,
order_direction: order_direction
}
this.activeSortField = order_field
this.activeSortDirection = order_direction
apiClient.listRecipeBooks(options).then((result) => {
this.cookbooks = result.data
})
},
isActiveSort: function(field, direction) {
// Check if the current item is the active sorting option
return this.activeSortField === field && this.activeSortDirection === direction;
},
handleEditButton: function(){
if (!this.manSubmitted){
this.submitText = "Back"
this.manSubmitted = true
} else {
this.submitText = "Edit"
this.manSubmitted = false
}
},
updateManualSorting: function(){
let old_order = Object.assign({}, this.cookbooks);
let promises = []
this.cookbooks.forEach((element, index) => {
let apiClient = new ApiApiFactory()
promises.push(apiClient.partialUpdateManualOrderBooks(element.id, {order: index}))
})
return Promise.all(promises).then(() => {
}).catch((err) => {
this.cookbooks = old_order
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
},
directives: { directives: {
hover: { hover: {
inserted: function (el) { inserted: function (el) {

View File

@ -9172,6 +9172,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
const localVarQueryParameter = {} as any; const localVarQueryParameter = {} as any;
if (options.order_field !== undefined) {
localVarQueryParameter['order_field'] = options.order_field;
}
if (options.order_direction!== undefined) {
localVarQueryParameter['order_direction'] = options.order_direction;
}
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
@ -10263,6 +10270,40 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @param {string} id A unique integer value identifying the book.
* @param {RecipeBook} [recipeBook]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateManualOrderBooks: async (id: string, recipeBook?: RecipeBook , options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/recipe-book/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(recipeBook , localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @param {string} id A unique integer value identifying this access token. * @param {string} id A unique integer value identifying this access token.
@ -16430,6 +16471,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.moveKeyword(id, parent, keyword, options); const localVarAxiosArgs = await localVarAxiosParamCreator.moveKeyword(id, parent, keyword, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {RecipeBook} [recipeBook]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async partialUpdateManualOrderBooks(id: string, recipeBook?: RecipeBook, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategoryRelation>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateManualOrderBooks(id, recipeBook, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {string} id A unique integer value identifying this access token. * @param {string} id A unique integer value identifying this access token.
@ -19121,6 +19173,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
moveKeyword(id: string, parent: string, keyword?: Keyword, options?: any): AxiosPromise<Keyword> { moveKeyword(id: string, parent: string, keyword?: Keyword, options?: any): AxiosPromise<Keyword> {
return localVarFp.moveKeyword(id, parent, keyword, options).then((request) => request(axios, basePath)); return localVarFp.moveKeyword(id, parent, keyword, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {RecipeBook} [recipeBook]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateManualOrderBooks(id: string, recipeBook?: RecipeBook, options?: any): AxiosPromise<SupermarketCategoryRelation> {
return localVarFp.partialUpdateManualOrderBooks(id, recipeBook, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {string} id A unique integer value identifying this access token. * @param {string} id A unique integer value identifying this access token.
@ -20707,7 +20769,6 @@ export class ApiApi extends BaseAPI {
public createRecipeBookEntry(recipeBookEntry?: RecipeBookEntry, options?: any) { public createRecipeBookEntry(recipeBookEntry?: RecipeBookEntry, options?: any) {
return ApiApiFp(this.configuration).createRecipeBookEntry(recipeBookEntry, options).then((request) => request(this.axios, this.basePath)); return ApiApiFp(this.configuration).createRecipeBookEntry(recipeBookEntry, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json and images * function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json and images
* @param {any} [body] * @param {any} [body]
@ -21951,7 +22012,16 @@ export class ApiApi extends BaseAPI {
public moveKeyword(id: string, parent: string, keyword?: Keyword, options?: any) { public moveKeyword(id: string, parent: string, keyword?: Keyword, options?: any) {
return ApiApiFp(this.configuration).moveKeyword(id, parent, keyword, options).then((request) => request(this.axios, this.basePath)); return ApiApiFp(this.configuration).moveKeyword(id, parent, keyword, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {RecipeBook} [recipeBook]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public partialUpdateManualOrderBooks(id: string, recipeBook?: RecipeBook, options?: any) {
return ApiApiFp(this.configuration).partialUpdateManualOrderBooks(id, recipeBook, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {string} id A unique integer value identifying this access token. * @param {string} id A unique integer value identifying this access token.