From 6fc9597d207bbba6026bd4712d739717165606fc Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 10 Feb 2020 01:00:00 +0100 Subject: [PATCH] Add support for sorting categories --- src/action/index.ts | 1 + src/action/serialization/parentaction.ts | 4 ++ src/action/updatecategorysorting.ts | 56 +++++++++++++++++++ src/api/validator.ts | 25 +++++++++ src/database/category.ts | 20 ++++++- .../20200210-add-category-sorting.ts | 39 +++++++++++++ .../dispatch-parent-action/createcategory.ts | 16 +++++- .../dispatch-parent-action/index.ts | 4 ++ .../updatecategorysorting.ts | 45 +++++++++++++++ src/function/sync/get-server-data-status.ts | 9 ++- src/object/serverdatastatus.ts | 1 + 11 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 src/action/updatecategorysorting.ts create mode 100644 src/database/migration/migrations/20200210-add-category-sorting.ts create mode 100644 src/function/sync/apply-actions/dispatch-parent-action/updatecategorysorting.ts diff --git a/src/action/index.ts b/src/action/index.ts index def6420..0098b6b 100644 --- a/src/action/index.ts +++ b/src/action/index.ts @@ -55,6 +55,7 @@ export { UpdateAppActivitiesAction } from './updateappactivities' export { UpdateCategoryBatteryLimitAction } from './updatecategorybatterylimit' export { UpdateCategoryBlockAllNotificationsAction } from './updatecategoryblockallnotifications' export { UpdateCategoryBlockedTimesAction } from './updatecategoryblockedtimes' +export { UpdateCategorySortingAction } from './updatecategorysorting' export { UpdateCategoryTemporarilyBlockedAction } from './updatecategorytemporarilyblocked' export { UpdateCategoryTimeWarningsAction } from './updatecategorytimewarnings' export { UpdateCategoryTitleAction } from './updatecategorytitle' diff --git a/src/action/serialization/parentaction.ts b/src/action/serialization/parentaction.ts index dbd9f53..e0d5da0 100644 --- a/src/action/serialization/parentaction.ts +++ b/src/action/serialization/parentaction.ts @@ -45,6 +45,7 @@ import { SerializedSetUserTimezoneAction, SetUserTimezoneAction } from '../setus import { SerializedUpdateCategoryBatteryLimitAction, UpdateCategoryBatteryLimitAction } from '../updatecategorybatterylimit' import { SerializedUpdateCategoryBlockAllNotificationsAction, UpdateCategoryBlockAllNotificationsAction } from '../updatecategoryblockallnotifications' import { SerializedUpdateCategoryBlockedTimesAction, UpdateCategoryBlockedTimesAction } from '../updatecategoryblockedtimes' +import { SerializedUpdateCategorySortingAction, UpdateCategorySortingAction } from '../updatecategorysorting' import { SerializedUpdateCategoryTemporarilyBlockedAction, UpdateCategoryTemporarilyBlockedAction } from '../updatecategorytemporarilyblocked' import { SerializedUpdateCategoryTimeWarningsAction, UpdateCategoryTimeWarningsAction } from '../updatecategorytimewarnings' import { SerializedUpdateCategoryTitleAction, UpdateCategoryTitleAction } from '../updatecategorytitle' @@ -85,6 +86,7 @@ export type SerializedParentAction = SerializedUpdateCategoryBatteryLimitAction | SerializedUpdateCategoryBlockAllNotificationsAction | SerializedUpdateCategoryBlockedTimesAction | + SerializedUpdateCategorySortingAction | SerializedUpdateCategoryTemporarilyBlockedAction | SerializedUpdateCategoryTimeWarningsAction | SerializedUpdateCategoryTitleAction | @@ -154,6 +156,8 @@ export const parseParentAction = (action: SerializedParentAction): ParentAction return UpdateCategoryBlockAllNotificationsAction.parse(action) } else if (action.type === 'UPDATE_CATEGORY_BLOCKED_TIMES') { return UpdateCategoryBlockedTimesAction.parse(action) + } else if (action.type === 'UPDATE_CATEGORY_SORTING') { + return UpdateCategorySortingAction.parse(action) } else if (action.type === 'UPDATE_CATEGORY_TIME_WARNINGS') { return UpdateCategoryTimeWarningsAction.parse(action) } else if (action.type === 'UPDATE_CATEGORY_TITLE') { diff --git a/src/action/updatecategorysorting.ts b/src/action/updatecategorysorting.ts new file mode 100644 index 0000000..3b58b48 --- /dev/null +++ b/src/action/updatecategorysorting.ts @@ -0,0 +1,56 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { uniq } from 'lodash' +import { assertIdWithinFamily } from '../util/token' +import { ParentAction } from './basetypes' + +export class UpdateCategorySortingAction extends ParentAction { + readonly categoryIds: Array + + constructor ({ categoryIds }: { + categoryIds: Array + }) { + super() + + if (categoryIds.length === 0) { + throw new Error('empty category sorting list') + } + + if (uniq(categoryIds).length !== categoryIds.length) { + throw new Error('category sorting list has duplicates') + } + + categoryIds.forEach((categoryId) => assertIdWithinFamily(categoryId)) + + this.categoryIds = categoryIds + } + + serialize = (): SerializedUpdateCategorySortingAction => ({ + type: 'UPDATE_CATEGORY_SORTING', + categoryIds: this.categoryIds + }) + + static parse = ({ categoryIds }: SerializedUpdateCategorySortingAction) => ( + new UpdateCategorySortingAction({ categoryIds }) + ) +} + +export interface SerializedUpdateCategorySortingAction { + type: 'UPDATE_CATEGORY_SORTING' + categoryIds: Array +} diff --git a/src/api/validator.ts b/src/api/validator.ts index d2d1386..d15823b 100644 --- a/src/api/validator.ts +++ b/src/api/validator.ts @@ -839,6 +839,28 @@ const definitions = { "type" ] }, + "SerializedUpdateCategorySortingAction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "UPDATE_CATEGORY_SORTING" + ] + }, + "categoryIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "categoryIds", + "type" + ] + }, "SerializedUpdateCategoryTemporarilyBlockedAction": { "type": "object", "properties": { @@ -1699,6 +1721,9 @@ export const isSerializedParentAction: (value: object) => value is SerializedPar { "$ref": "#/definitions/SerializedUpdateCategoryBlockedTimesAction" }, + { + "$ref": "#/definitions/SerializedUpdateCategorySortingAction" + }, { "$ref": "#/definitions/SerializedUpdateCategoryTemporarilyBlockedAction" }, diff --git a/src/database/category.ts b/src/database/category.ts index 504de2f..eef1c70 100644 --- a/src/database/category.ts +++ b/src/database/category.ts @@ -57,9 +57,13 @@ export interface CategoryAttributesVersion6 { temporarilyBlockedEndTime: number } +export interface CategoryAttributesVersion7 { + sort: number +} + export type CategoryAttributes = CategoryAttributesVersion1 & CategoryAttributesVersion2 & CategoryAttributesVersion3 & CategoryAttributesVersion4 & CategoryAttributesVersion5 & - CategoryAttributesVersion6 + CategoryAttributesVersion6 & CategoryAttributesVersion7 export type CategoryModel = Sequelize.Model & CategoryAttributes export type CategoryModelStatic = typeof Sequelize.Model & { @@ -157,13 +161,25 @@ export const attributesVersion6: SequelizeAttributes } } +export const attributesVersion7: SequelizeAttributes = { + sort: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0, + validate: { + min: 0 + } + } +} + export const attributes: SequelizeAttributes = { ...attributesVersion1, ...attributesVersion2, ...attributesVersion3, ...attributesVersion4, ...attributesVersion5, - ...attributesVersion6 + ...attributesVersion6, + ...attributesVersion7 } export const createCategoryModel = (sequelize: Sequelize.Sequelize): CategoryModelStatic => sequelize.define('Category', attributes) as CategoryModelStatic diff --git a/src/database/migration/migrations/20200210-add-category-sorting.ts b/src/database/migration/migrations/20200210-add-category-sorting.ts new file mode 100644 index 0000000..9cb2ce9 --- /dev/null +++ b/src/database/migration/migrations/20200210-add-category-sorting.ts @@ -0,0 +1,39 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { QueryInterface, Sequelize, Transaction } from 'sequelize' +import { attributesVersion7 as categoryAttributes } from '../../category' + +export async function up (queryInterface: QueryInterface, sequelize: Sequelize) { + await sequelize.transaction({ + type: Transaction.TYPES.EXCLUSIVE + }, async (transaction) => { + await queryInterface.addColumn('Categories', 'sort', { + ...categoryAttributes.sort + }, { + transaction + }) + }) +} + +export async function down (queryInterface: QueryInterface, sequelize: Sequelize) { + await sequelize.transaction({ + type: Transaction.TYPES.EXCLUSIVE + }, async (transaction) => { + await queryInterface.removeColumn('Categories', 'sort', { transaction }) + }) +} diff --git a/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts b/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts index ee16090..484749b 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -37,6 +37,17 @@ export async function dispatchCreateCategory ({ action, cache }: { throw new Error('missing child for new category') } + const oldMaxSort: number = await cache.database.category.max('sort', { + transaction: cache.transaction, + where: { + familyId: cache.familyId, + childId: action.childId + } + }) + + // if there are no categories, then this is not a number + const sort = Number.isSafeInteger(oldMaxSort + 1) ? (oldMaxSort + 1) : 0 + // no version number needs to be updated await cache.database.category.create({ familyId: cache.familyId, @@ -53,7 +64,8 @@ export async function dispatchCreateCategory ({ action, cache }: { usedTimesVersion: generateVersionId(), parentCategoryId: '', blockAllNotifications: false, - timeWarningFlags: 0 + timeWarningFlags: 0, + sort }, { transaction: cache.transaction }) // update the cache diff --git a/src/function/sync/apply-actions/dispatch-parent-action/index.ts b/src/function/sync/apply-actions/dispatch-parent-action/index.ts index 4ce2b0c..87999b8 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/index.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/index.ts @@ -46,6 +46,7 @@ import { UpdateCategoryBatteryLimitAction, UpdateCategoryBlockAllNotificationsAction, UpdateCategoryBlockedTimesAction, + UpdateCategorySortingAction, UpdateCategoryTemporarilyBlockedAction, UpdateCategoryTimeWarningsAction, UpdateCategoryTitleAction, @@ -86,6 +87,7 @@ import { dispatchSetUserTimezone } from './setusertimezone' import { dispatchUpdateCategoryBatteryLimit } from './updatecategorybatterylimit' import { dispatchUpdateCategoryBlockAllNotifications } from './updatecategoryblockallnotifications' import { dispatchUpdateCategoryBlockedTimes } from './updatecategoryblockedtimes' +import { dispatchUpdateCategorySorting } from './updatecategorysorting' import { dispatchUpdateCategoryTemporarilyBlocked } from './updatecategorytemporarilyblocked' import { dispatchUpdateCategoryTimeWarnings } from './updatecategorytimewarnings' import { dispatchUpdateCategoryTitle } from './updatecategorytitle' @@ -148,6 +150,8 @@ export const dispatchParentAction = async ({ action, cache, parentUserId, source await dispatchUpdateCategoryBlockAllNotifications({ action, cache }) } else if (action instanceof UpdateCategoryBlockedTimesAction) { await dispatchUpdateCategoryBlockedTimes({ action, cache }) + } else if (action instanceof UpdateCategorySortingAction) { + await dispatchUpdateCategorySorting({ action, cache }) } else if (action instanceof IncrementCategoryExtraTimeAction) { await dispatchIncrementCategoryExtraTime({ action, cache }) } else if (action instanceof UpdateCategoryTemporarilyBlockedAction) { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorysorting.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorysorting.ts new file mode 100644 index 0000000..6948cc8 --- /dev/null +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorysorting.ts @@ -0,0 +1,45 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { UpdateCategorySortingAction } from '../../../../action' +import { Cache } from '../cache' + +export async function dispatchUpdateCategorySorting ({ action, cache }: { + action: UpdateCategorySortingAction + cache: Cache +}) { + // no validation here: + // - only parents can do it + // - using it over categories which don't belong together destroys the sorting for both, + // but does not cause any trouble + + for (let i = 0; i < action.categoryIds.length; i++) { + const categoryId = action.categoryIds[i] + + await cache.database.category.update({ + sort: i + }, { + transaction: cache.transaction, + where: { + familyId: cache.familyId, + categoryId + } + }) + + cache.categoriesWithModifiedBaseData.push(categoryId) + } +} diff --git a/src/function/sync/get-server-data-status.ts b/src/function/sync/get-server-data-status.ts index 563f4fa..ebc7d2c 100644 --- a/src/function/sync/get-server-data-status.ts +++ b/src/function/sync/get-server-data-status.ts @@ -342,7 +342,8 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI 'timeWarningFlags', 'minBatteryCharging', 'minBatteryMobile', - 'temporarilyBlockedEndTime' + 'temporarilyBlockedEndTime', + 'sort' ], transaction })).map((item) => ({ @@ -358,7 +359,8 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI timeWarningFlags: item.timeWarningFlags, minBatteryCharging: item.minBatteryCharging, minBatteryMobile: item.minBatteryMobile, - temporarilyBlockedEndTime: item.temporarilyBlockedEndTime + temporarilyBlockedEndTime: item.temporarilyBlockedEndTime, + sort: item.sort })) result.categoryBase = dataForSyncing.map((item): ServerUpdatedCategoryBaseData => ({ @@ -374,7 +376,8 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI timeWarnings: item.timeWarningFlags, mblMobile: item.minBatteryMobile, mblCharging: item.minBatteryCharging, - tempBlockTime: item.temporarilyBlockedEndTime + tempBlockTime: item.temporarilyBlockedEndTime, + sort: item.sort })) } diff --git a/src/object/serverdatastatus.ts b/src/object/serverdatastatus.ts index a58efc4..108b9c0 100644 --- a/src/object/serverdatastatus.ts +++ b/src/object/serverdatastatus.ts @@ -108,6 +108,7 @@ export interface ServerUpdatedCategoryBaseData { // mbl = minimum battery level mblCharging: number mblMobile: number + sort: number } export interface ServerUpdatedCategoryAssignedApps {