89-poster-defaults (#96)
* print defaults in printproducts * Add default to printproduct mutation * added insertDefaults when changing productgroup * fixed bug for square in square * cleanup, and tests for calculations * small fixes, added return values for update
This commit is contained in:
+2
-1
@@ -7,7 +7,8 @@
|
||||
"exportSQL": "cd src/__test__/db_generator/ && npm install && node index.js && cd ../../../",
|
||||
"test": "npm run exportSQL && jest --detectOpenHandles",
|
||||
"test:dev": "npm run exportSQL && jest --watch",
|
||||
"unit:dev": "jest --watch src/__test__/unit.test.ts",
|
||||
"unit": "jest --testPathIgnorePatterns=\"src/__test__/all.test.ts\"",
|
||||
"unit:dev": "jest --testPathIgnorePatterns=\"src/__test__/all.test.ts\" --watch src/**/*",
|
||||
"test:manual": "cd manual-tests && ts-node test-scopes.ts"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('GraphQL Apollo unit tests', () => {
|
||||
expect(() => checkAccess(['orders.read'], auth)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test auth not ok for test', () => {
|
||||
it('user bla is accepted', () => {
|
||||
const auth: ClaimVerifyResult = {
|
||||
@@ -86,6 +87,7 @@ describe('GraphQL Apollo unit tests', () => {
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test id auth no enough', () => {
|
||||
it('user test is accepted', () => {
|
||||
const auth = JSON.parse(JSON.stringify(idTokenAuth)) as ClaimVerifyResult;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,245 @@
|
||||
import { Maybe } from 'graphql/jsutils/Maybe';
|
||||
import {
|
||||
BorderType,
|
||||
PrintProductDefaults,
|
||||
} from '../types/printproduct-defaults-types';
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
|
||||
type TPosterDefaultPositioning = {
|
||||
width_mm: number;
|
||||
height_mm: number;
|
||||
crop_x: number;
|
||||
crop_y: number;
|
||||
};
|
||||
export const calculateDefaultPositioning = (
|
||||
productWidth: number,
|
||||
productHeight: number,
|
||||
focusXpoint2: number,
|
||||
focusYpoint2: number,
|
||||
): TPosterDefaultPositioning => {
|
||||
// Because we dont want to be dependent on different environments
|
||||
// number of supported decimals to round to 10 decimal places.
|
||||
// And I know 70/50 is 1.4 so its not needed but added for clarity.
|
||||
const size70x50ratio = parseFloat((70 / 50).toFixed(10));
|
||||
const size50x70ratio = parseFloat((50 / 70).toFixed(10));
|
||||
const width_height_ratio = productWidth / productHeight;
|
||||
|
||||
// If focuspoints are null we set to 50 to make calculations
|
||||
// below a bit clearer.
|
||||
const fpX = focusXpoint2 === null ? 50 : focusXpoint2;
|
||||
const fpY = focusYpoint2 === null ? 50 : focusYpoint2;
|
||||
|
||||
const width_mm = width_height_ratio >= size70x50ratio ? 700 : 500;
|
||||
const height_mm = width_height_ratio <= size50x70ratio ? 700 : 500;
|
||||
|
||||
// Calculate where to put the crop_x percent value
|
||||
// We place the center of the crop area as close to
|
||||
// the focusXpoint2 and focusYpoint2 values. We do
|
||||
// different calculations depending on if the poster size
|
||||
// orientation (landscape, portrait or square) is inside
|
||||
// the motif orientation.
|
||||
//
|
||||
// The goal for the calculations is to get the smallest amount of area
|
||||
// that we crop away.
|
||||
const crop_x = (() => {
|
||||
if (width_height_ratio >= size70x50ratio) {
|
||||
const centerOfRatioArea = (productHeight * 1.4) / 2;
|
||||
let x = Math.max(centerOfRatioArea, (fpX / 100) * productWidth); // Place center close to focuspointX but not outside the left edge
|
||||
x = Math.min(productWidth - centerOfRatioArea, x); // Dont place outside the right edge
|
||||
x = x - centerOfRatioArea; // Convert center to left edge
|
||||
x = x / productWidth; // Convert to percent;
|
||||
return x;
|
||||
} else if (
|
||||
// Square in landscape
|
||||
width_height_ratio > 1 &&
|
||||
width_height_ratio < size70x50ratio
|
||||
) {
|
||||
const centerOfRatioArea = productHeight / 2;
|
||||
let x = Math.max(centerOfRatioArea, (fpX / 100) * productWidth); // Place center close to focuspointX but not outside the left side
|
||||
x = Math.min(productWidth - centerOfRatioArea, x); // Dont place outside the right edge
|
||||
x = x - centerOfRatioArea; // Convert center to left edge
|
||||
x = x / productWidth; // Convert to percent
|
||||
return x;
|
||||
} else {
|
||||
// If the scenario is, square in portrait, portrait in portrait or square
|
||||
// in square then we are placing vertical, therefor x will be 0.
|
||||
return 0;
|
||||
}
|
||||
})();
|
||||
|
||||
// Read comment for crop_x above.
|
||||
const crop_y = (() => {
|
||||
if (width_height_ratio < 1 && width_height_ratio > size50x70ratio) {
|
||||
// Square in portrait
|
||||
const centerOfRatioArea = productWidth / 2;
|
||||
let y = Math.max(centerOfRatioArea, (fpY / 100) * productHeight); // Place center close to focuspointY but not ouside top edge
|
||||
y = Math.min(productHeight - centerOfRatioArea, y); // Dont place outside bottom edge
|
||||
y = y - centerOfRatioArea; // Convert center to top edge
|
||||
y = y / productHeight; // Convert to percent
|
||||
return y;
|
||||
} else if (width_height_ratio <= size50x70ratio) {
|
||||
// Portrait in portrait
|
||||
const centerOfRatioArea = productWidth / size50x70ratio / 2;
|
||||
let y = Math.max(centerOfRatioArea, (fpY / 100) * productHeight); // Place center close to focuspointY but not ouside top edge
|
||||
y = Math.min(productHeight - centerOfRatioArea, y); // Dont place outside bottom edge
|
||||
y = y - centerOfRatioArea; // Convert center to top edge
|
||||
y = y / productHeight; // Convert to percent
|
||||
return y;
|
||||
} else {
|
||||
// If the scenario is landscape in landscape, square in landscape or square in square
|
||||
// the we are placing horizontal and therefor y will be 0.
|
||||
return 0;
|
||||
}
|
||||
})();
|
||||
|
||||
return { width_mm, height_mm, crop_x, crop_y };
|
||||
};
|
||||
|
||||
export class PrintProductDefaultsAPI extends BaseSQLDataSource {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @function getBorderType
|
||||
* @description Will convert lowercase none | white | black to
|
||||
* uppercase and define it as a BorderType
|
||||
*/
|
||||
getBorderType(dbValue: string): string {
|
||||
switch (dbValue) {
|
||||
case 'white':
|
||||
return BorderType[BorderType.WHITE];
|
||||
case 'black':
|
||||
return BorderType[BorderType.BLACK];
|
||||
default:
|
||||
return BorderType[BorderType.NONE];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @function getDefaults
|
||||
* @description This function will fetch the default values from printproducts_defaults table
|
||||
* with the printId as a key. Border is defined in lowercase in DB but returned in uppercase
|
||||
* to keep consistency in both DB and graphql.
|
||||
*/
|
||||
async getDefaults(printId: number): Promise<Maybe<PrintProductDefaults>> {
|
||||
return this.knex
|
||||
.column(
|
||||
{ printId: 'print_id' },
|
||||
{ widthMm: 'width_mm' },
|
||||
{ heightMm: 'height_mm' },
|
||||
{ cropX: 'crop_x' },
|
||||
{ cropY: 'crop_y' },
|
||||
'border',
|
||||
)
|
||||
.select()
|
||||
.from('printproducts_defaults')
|
||||
.where('print_id', printId)
|
||||
.first()
|
||||
.then((row) => {
|
||||
if (row) {
|
||||
return Object.assign(
|
||||
{
|
||||
...row,
|
||||
},
|
||||
{ border: this.getBorderType(row.border) },
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @function insertDefaults
|
||||
* @description Inserts a new printproduct into printproducts_defaults, sets the default
|
||||
* values the same way we did when settings default values for all older printproducts.
|
||||
*/
|
||||
async insertDefaults(printId: number): Promise<0 | 1> {
|
||||
const product = await this.knex.raw(
|
||||
`
|
||||
SELECT width, height, "focusXpoint2", "focusYpoint2" FROM
|
||||
"product-printproducts" ppp
|
||||
JOIN v_product vp ON ppp.productid = vp.id
|
||||
WHERE printid = ?`,
|
||||
[printId],
|
||||
);
|
||||
|
||||
const { width, height, focusXpoint2, focusYpoint2 } = product.rows[0];
|
||||
|
||||
// GUARDS
|
||||
if (!width || !height)
|
||||
throw new Error(
|
||||
'PrintProductDefaultsAPI.insertDefaults: width and height must be defined for a motif to be able to set defaults',
|
||||
);
|
||||
|
||||
const { width_mm, height_mm, crop_x, crop_y } = calculateDefaultPositioning(
|
||||
width,
|
||||
height,
|
||||
focusXpoint2,
|
||||
focusYpoint2,
|
||||
);
|
||||
|
||||
return this.knex
|
||||
.raw(
|
||||
`
|
||||
INSERT INTO printproducts_defaults (print_id, width_mm, height_mm, crop_x, crop_y, border)
|
||||
VALUES (?, ?, ?, ?, ?, 'none');
|
||||
`,
|
||||
[printId, width_mm, height_mm, crop_x, crop_y],
|
||||
)
|
||||
.catch((e) => {
|
||||
throw new Error(e);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MUTATIONS
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @function updateDefaults
|
||||
* @description Updates or inserts default values for a printproduct in the
|
||||
* table printproducts_defaults.
|
||||
*/
|
||||
async updateDefaults(
|
||||
printId: number,
|
||||
widthMm: number,
|
||||
heightMm: number,
|
||||
cropX: number,
|
||||
cropY: number,
|
||||
border: 'none' | 'white' | 'black',
|
||||
): Promise<PrintProductDefaults> {
|
||||
return this.knex
|
||||
.table('printproducts_defaults')
|
||||
.update(
|
||||
{
|
||||
width_mm: widthMm,
|
||||
height_mm: heightMm,
|
||||
crop_x: cropX,
|
||||
crop_y: cropY,
|
||||
border: border.toLocaleLowerCase(),
|
||||
},
|
||||
['print_id', 'width_mm', 'height_mm', 'crop_x', 'crop_y', 'border'],
|
||||
)
|
||||
.where({ print_id: printId })
|
||||
.then((row) => {
|
||||
if (row) {
|
||||
const first = row[0];
|
||||
return Object.assign(
|
||||
{
|
||||
...first,
|
||||
},
|
||||
{ border: this.getBorderType(first.border) },
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
throw new Error(
|
||||
`Something went wrong when trying to update with printid [${printId}][${e.message}]`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,18 @@ import {
|
||||
import { UserInputError } from 'apollo-server';
|
||||
import { sendPathChangedCmd } from '../bernard/client';
|
||||
import { TextsAPI } from './texts-api';
|
||||
import { PrintProductDefaultsAPI } from './printproduct-defaults-api';
|
||||
|
||||
const MINUTE = 60;
|
||||
|
||||
export class ProductAPI extends BaseSQLDataSource {
|
||||
textsApi: TextsAPI;
|
||||
constructor(config, textsApi) {
|
||||
printProductDefaultsApi: PrintProductDefaultsAPI;
|
||||
|
||||
constructor(config, textsApi, printProductDefaultsApi) {
|
||||
super(config);
|
||||
this.textsApi = textsApi;
|
||||
this.printProductDefaultsApi = printProductDefaultsApi;
|
||||
}
|
||||
|
||||
// async getMaterial(printId: number): Promise<any> {
|
||||
@@ -607,12 +611,20 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
// Delete from product-printproducts_materials
|
||||
const printProductIds = shouldDelete.map((item) => item.printid);
|
||||
|
||||
// Delete from materials first, must be in sequence since we have constraint in materials to printproducts
|
||||
// Delete from materials first, must be in sequence
|
||||
// since we have constraint in materials to printproducts
|
||||
await this.knex
|
||||
.from('product-printproducts_materials')
|
||||
.whereIn('printid', printProductIds)
|
||||
.del();
|
||||
|
||||
// Delete from printproducts_defaults, same here run
|
||||
// this first because of constraint.
|
||||
await this.knex
|
||||
.from('printproducts_defaults')
|
||||
.whereIn('printId', printProductIds)
|
||||
.del();
|
||||
|
||||
// Delete from product-printproducts
|
||||
await this.knex
|
||||
.from('product-printproducts')
|
||||
@@ -648,6 +660,21 @@ export class ProductAPI extends BaseSQLDataSource {
|
||||
.where('productid', productId)
|
||||
.whereIn('groupid', shouldAdd);
|
||||
|
||||
// Add printproduct-defaults for all new printIds
|
||||
const insertDefaults = [];
|
||||
newPrintIds.forEach(({ printid, groupid }) => {
|
||||
// Possible to add for canvas and framed-print later
|
||||
if (groupid === ProductGroup.POSTER) {
|
||||
// Add printproduct defaults if poster
|
||||
insertDefaults.push(
|
||||
this.printProductDefaultsApi.insertDefaults(printid),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Run parallell
|
||||
await Promise.all(insertDefaults);
|
||||
|
||||
// Insert printIds into product-printproducts_materials
|
||||
const insertMaterials = [];
|
||||
const insertMaterialQuery = `INSERT INTO "product-printproducts_materials" (printid, materialid) VALUES (?, ?)`;
|
||||
|
||||
+9
-1
@@ -19,6 +19,8 @@ import { verifyToken } from './cognito/cognito-client';
|
||||
import { readFileSync } from 'fs';
|
||||
import { InteriorsLambdaAPI } from './datasources/interiors-lambda-api';
|
||||
import { TextsAPI } from './datasources/texts-api';
|
||||
import { PrintProductDefaultsAPI } from './datasources/printproduct-defaults-api';
|
||||
|
||||
const path = require('path');
|
||||
const typeDefs = readFileSync(
|
||||
path.join(__dirname, './schema.graphql'),
|
||||
@@ -35,14 +37,20 @@ const designerApi = new DesignerAPI(knexConfig);
|
||||
const imageServerApi = new ImageServerApi();
|
||||
const interiorsLambdaApi = new InteriorsLambdaAPI();
|
||||
const interiorApi = new InteriorAPI(knexConfig, imageServerApi);
|
||||
const printProductDefaultsApi = new PrintProductDefaultsAPI(knexConfig);
|
||||
const keywordApi = new KeywordAPI(knexConfig);
|
||||
const orderApi = new OrderAPI(knexConfig);
|
||||
const productApi = new ProductAPI(knexConfig, textsApi);
|
||||
const productApi = new ProductAPI(
|
||||
knexConfig,
|
||||
textsApi,
|
||||
printProductDefaultsApi,
|
||||
);
|
||||
|
||||
const dataSources = () => ({
|
||||
categoryApi,
|
||||
designerApi,
|
||||
interiorApi,
|
||||
printProductDefaultsApi,
|
||||
interiorsLambdaApi,
|
||||
imageServerApi,
|
||||
keywordApi,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ProductAPI } from '../datasources/product-api';
|
||||
import {
|
||||
PrintProduct,
|
||||
Product,
|
||||
ProductListResult,
|
||||
ProductsFilterInput,
|
||||
@@ -17,6 +18,8 @@ import { IdNumberResult } from '../types/types';
|
||||
import { moveS3File } from '../aws/s3';
|
||||
import { DesignerAPI } from '../datasources/designer-api';
|
||||
import { checkAccess } from '../cognito/access-control';
|
||||
import { PrintProductDefaultsAPI } from '../datasources/printproduct-defaults-api';
|
||||
import { PrintProductDefaults } from '../types/printproduct-defaults-types';
|
||||
|
||||
const ProductBlacklist = {
|
||||
async market(parent, _args, { dataSources }) {
|
||||
@@ -50,6 +53,11 @@ const PrintProduct = {
|
||||
async interiors({ id }, _args, { dataSources }) {
|
||||
return (<InteriorAPI>dataSources.interiorApi).getPrintProductInteriors(id);
|
||||
},
|
||||
async defaults({ id }, _args, { dataSources }) {
|
||||
return (<PrintProductDefaultsAPI>(
|
||||
dataSources.printProductDefaultsApi
|
||||
)).getDefaults(id);
|
||||
},
|
||||
};
|
||||
|
||||
async function getProductsResult(
|
||||
@@ -276,6 +284,21 @@ export const productMutationTypeDefs = {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function printProductDefaults
|
||||
* @description Add default values to a printproduct, this is used through admin gui.
|
||||
*/
|
||||
async printProductDefaults(
|
||||
_,
|
||||
{ printId, widthMm, heightMm, cropX, cropY, border },
|
||||
{ dataSources, auth },
|
||||
): Promise<PrintProductDefaults> {
|
||||
checkAccess(['products.write'], auth);
|
||||
return (<PrintProductDefaultsAPI>(
|
||||
dataSources.printProductDefaultsApi
|
||||
)).updateDefaults(printId, widthMm, heightMm, cropX, cropY, border);
|
||||
},
|
||||
|
||||
/**
|
||||
* @function productWallpaperType
|
||||
* @description Set the wallpapertype for a wallpaper product
|
||||
|
||||
+51
-16
@@ -390,6 +390,7 @@ type PrintProduct {
|
||||
inserted: DateTime!
|
||||
updated: DateTime
|
||||
interiors: [Interior]
|
||||
defaults: PrintProductDefaults
|
||||
}
|
||||
|
||||
enum InteriorType {
|
||||
@@ -413,6 +414,21 @@ type Interior {
|
||||
orientation: Orientation
|
||||
}
|
||||
|
||||
enum BorderType {
|
||||
NONE
|
||||
WHITE
|
||||
BLACK
|
||||
}
|
||||
|
||||
type PrintProductDefaults {
|
||||
printId: ID
|
||||
widthMm: Float
|
||||
heightMm: Float
|
||||
cropX: Float
|
||||
cropY: Float
|
||||
border: BorderType
|
||||
}
|
||||
|
||||
type Designer {
|
||||
id: ID!
|
||||
name: String
|
||||
@@ -476,32 +492,29 @@ type IdNumberResult {
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
||||
""" Product """
|
||||
addProduct(name: String!, batch: String): Product
|
||||
productInfo(productId: Int!, info: ProductInfoInput!): Product
|
||||
productFocusPoint(
|
||||
productWallpaperType(
|
||||
productId: Int!
|
||||
focusXpoint2: Float!
|
||||
focusYpoint2: Float!
|
||||
wallpaperTypes: [ProductWallpaperType]!
|
||||
): Product
|
||||
productBlacklisting(
|
||||
productId: Int!
|
||||
markets: [ProductBlacklistingInput]
|
||||
): Product
|
||||
productGroup(productId: Int!, groupIds: [Int]): Product
|
||||
productGroupInteriors(productId: Int!, groupId: Int!, uris: [String]): Product
|
||||
productKeywords(productId: Int!, keywordIds: [Int]): Product
|
||||
|
||||
""" Product Image """
|
||||
productFocusPoint(
|
||||
productId: Int!
|
||||
focusXpoint2: Float!
|
||||
focusYpoint2: Float!
|
||||
): Product
|
||||
productProportionsWarning(
|
||||
productId: Int!
|
||||
proportions: ProductProportionsInput!
|
||||
): Product
|
||||
addOwnInteriorToPrintProduct(
|
||||
printId: Int!
|
||||
uploadedS3Key: String!
|
||||
): IdNumberResult
|
||||
productWallpaperType(
|
||||
productId: Int!
|
||||
wallpaperTypes: [ProductWallpaperType]!
|
||||
): Product
|
||||
updateProductImage(
|
||||
productId: Int!
|
||||
uploadedS3Key: String!
|
||||
@@ -509,16 +522,38 @@ type Mutation {
|
||||
height: Int!
|
||||
): String
|
||||
|
||||
""" Product Relations """
|
||||
addRelatedProducts(productId: Int!, articleNumbers: [String]!): Product
|
||||
removeRelatedProducts(productId: Int!, relatedProductIds: [Int]!): Product
|
||||
|
||||
addKeyword(name: String!, type: KeywordType): Keyword
|
||||
|
||||
""" Product Categories """
|
||||
addCategoriesToProduct(productId: Int!, categoryIds: [Int]!): Product
|
||||
removeCategoriesFromProduct(productId: Int!, categoryIds: [Int]!): Product
|
||||
|
||||
""" Product Keywords """
|
||||
productKeywords(productId: Int!, keywordIds: [Int]): Product
|
||||
addKeyword(name: String!, type: KeywordType): Keyword
|
||||
|
||||
""" Product Other """
|
||||
updateProductComments(productId: Int!, comments: String!): Product
|
||||
|
||||
""" Printproducts """
|
||||
productGroup(productId: Int!, groupIds: [Int]): Product
|
||||
productGroupInteriors(productId: Int!, groupId: Int!, uris: [String]): Product
|
||||
addOwnInteriorToPrintProduct(
|
||||
printId: Int!
|
||||
uploadedS3Key: String!
|
||||
): IdNumberResult
|
||||
printProductDefaults(
|
||||
printId: Int!
|
||||
widthMm: Int!
|
||||
heightMm: Int!
|
||||
cropX: Float!
|
||||
cropY: Float!
|
||||
border: BorderType!
|
||||
): PrintProductDefaults
|
||||
|
||||
""" Mass Update """
|
||||
massUpdatePublishing(
|
||||
productIds: [Int]!
|
||||
visible: Boolean
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export enum BorderType {
|
||||
NONE = 0,
|
||||
WHITE = 1,
|
||||
BLACK = 2,
|
||||
}
|
||||
|
||||
export interface PrintProductDefaults {
|
||||
printId: number;
|
||||
widthMm: number;
|
||||
heightMm: number;
|
||||
cropX: number;
|
||||
cropY: number;
|
||||
border: string;
|
||||
}
|
||||
Reference in New Issue
Block a user