From e01bcbf03d775f4998a9a3fe559c2d4c745a218a Mon Sep 17 00:00:00 2001 From: Xziy Date: Thu, 5 Sep 2024 08:03:04 +0300 Subject: [PATCH] init junction table selectedMediaFile --- hook/bindAdminpanel.js | 6 +- .../ProductCatalog/ProductCatalog.d.ts | 1 - .../ProductCatalog/ProductCatalog.js | 3 - .../ProductCatalog/ProductCatalog.ts | 4 - libs/adminpanel/ProductMediaManager/Item.d.ts | 30 +++ libs/adminpanel/ProductMediaManager/Item.js | 168 +++++++++++++++++ libs/adminpanel/ProductMediaManager/Item.ts | 170 +++++++++++++++++ .../ProductMediaManager.d.ts | 14 ++ .../ProductMediaManager.js | 80 ++++++++ .../ProductMediaManager.ts | 92 +++++++++ libs/adminpanel/models/bind.d.ts | 7 +- libs/adminpanel/models/bind.js | 6 +- libs/adminpanel/models/lib/product.d.ts | 11 +- libs/adminpanel/models/lib/product.js | 175 +++++++++++++++++- libs/adminpanel/models/lib/product.ts | 168 ++++++++++++++++- models/MediaFile.d.ts | 7 +- models/MediaFile.js | 5 +- models/MediaFile.ts | 8 +- models/SelectedMediaFile.d.ts | 25 +++ models/SelectedMediaFile.js | 32 ++++ models/SelectedMediaFile.ts | 48 +++++ package.json | 14 +- translations/en.json | 67 +++++++ translations/ru.json | 66 +++++++ 24 files changed, 1176 insertions(+), 31 deletions(-) create mode 100644 libs/adminpanel/ProductMediaManager/Item.d.ts create mode 100644 libs/adminpanel/ProductMediaManager/Item.js create mode 100644 libs/adminpanel/ProductMediaManager/Item.ts create mode 100644 libs/adminpanel/ProductMediaManager/ProductMediaManager.d.ts create mode 100644 libs/adminpanel/ProductMediaManager/ProductMediaManager.js create mode 100644 libs/adminpanel/ProductMediaManager/ProductMediaManager.ts create mode 100644 models/SelectedMediaFile.d.ts create mode 100644 models/SelectedMediaFile.js create mode 100644 models/SelectedMediaFile.ts create mode 100644 translations/en.json create mode 100644 translations/ru.json diff --git a/hook/bindAdminpanel.js b/hook/bindAdminpanel.js index c2936e4d..94b819f7 100644 --- a/hook/bindAdminpanel.js +++ b/hook/bindAdminpanel.js @@ -1,6 +1,10 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = bindAdminpanel; +const bind_1 = __importDefault(require("../libs/adminpanel/models/bind")); const ProductCatalog_1 = require("../libs/adminpanel/ProductCatalog/ProductCatalog"); function bindAdminpanel() { sails.on('Adminpanel:afterHook:loaded', async () => { @@ -14,7 +18,7 @@ function bindAdminpanel() { function processBindAdminpanel() { if (sails.hooks?.adminpanel?.addModelConfig !== undefined) { const addModelConfig = sails.hooks.adminpanel.addModelConfig; - addModelConfig(settings); + addModelConfig(bind_1.default); } if (Array.isArray(sails.config.adminpanel?.sections)) { let baseRoute = sails.config.adminpanel.routePrefix; diff --git a/libs/adminpanel/ProductCatalog/ProductCatalog.d.ts b/libs/adminpanel/ProductCatalog/ProductCatalog.d.ts index 3cdd9e8a..d59d6b96 100644 --- a/libs/adminpanel/ProductCatalog/ProductCatalog.d.ts +++ b/libs/adminpanel/ProductCatalog/ProductCatalog.d.ts @@ -21,7 +21,6 @@ declare class BaseModelItem extends AbstractItem { }>; getChilds(parentId: string, catalogId: string): Promise; search(s: string, catalogId: string): Promise; - updateModelItems(itemId: string | number, data: T, catalogId: string): Promise; } export declare class Group extends BaseModelItem { name: string; diff --git a/libs/adminpanel/ProductCatalog/ProductCatalog.js b/libs/adminpanel/ProductCatalog/ProductCatalog.js index ed5a14f2..a193d218 100644 --- a/libs/adminpanel/ProductCatalog/ProductCatalog.js +++ b/libs/adminpanel/ProductCatalog/ProductCatalog.js @@ -73,9 +73,6 @@ class BaseModelItem extends AbstractCatalog_1.AbstractItem { }); return records.map((record) => this.toItem(record)); } - updateModelItems(itemId, data, catalogId) { - return Promise.resolve(undefined); - } } class Group extends BaseModelItem { constructor() { diff --git a/libs/adminpanel/ProductCatalog/ProductCatalog.ts b/libs/adminpanel/ProductCatalog/ProductCatalog.ts index 2aa07250..6dfe8427 100644 --- a/libs/adminpanel/ProductCatalog/ProductCatalog.ts +++ b/libs/adminpanel/ProductCatalog/ProductCatalog.ts @@ -97,10 +97,6 @@ class BaseModelItem extends AbstractItem { return records.map((record: ItemModel) => this.toItem(record) as T); } - - updateModelItems(itemId: string | number, data: T, catalogId: string): Promise { - return Promise.resolve(undefined); - } } diff --git a/libs/adminpanel/ProductMediaManager/Item.d.ts b/libs/adminpanel/ProductMediaManager/Item.d.ts new file mode 100644 index 00000000..e798a99a --- /dev/null +++ b/libs/adminpanel/ProductMediaManager/Item.d.ts @@ -0,0 +1,30 @@ +import { File, Item, UploaderFile, imageSizes } from 'sails-adminpanel/lib/media-manager/AbstractMediaManager'; +export declare class ImageItem extends File { + type: "application" | "audio" | "example" | "image" | "message" | "model" | "multipart" | "text" | "video"; + constructor(path: string, dir: string, model: string, metaModel: string); + getItems(limit: number, skip: number, sort: string): Promise<{ + data: Item[]; + next: boolean; + }>; + search(s: string): Promise; + upload(file: UploaderFile, filename: string, origFileName: string, imageSizes?: imageSizes | {}): Promise; + getChildren(id: string): Promise; + protected createSizes(file: UploaderFile, parent: Item, filename: string, imageSizes: imageSizes): Promise; + protected createThumb(id: string, file: UploaderFile, filename: string, origFileName: string): Promise; + protected createEmptyMeta(id: string): Promise; + getMeta(id: string): Promise<{ + key: string; + value: string; + }[]>; + setMeta(id: string, data: { + [p: string]: string; + }): Promise<{ + msg: "success"; + }>; + protected resizeImage(input: string, output: string, width: number, height: number): Promise; + uploadCropped(parent: Item, file: UploaderFile, filename: string, config: { + width: number; + height: number; + }): Promise; + delete(id: string): Promise; +} diff --git a/libs/adminpanel/ProductMediaManager/Item.js b/libs/adminpanel/ProductMediaManager/Item.js new file mode 100644 index 00000000..cf5b72f5 --- /dev/null +++ b/libs/adminpanel/ProductMediaManager/Item.js @@ -0,0 +1,168 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ImageItem = void 0; +const AbstractMediaManager_1 = require("sails-adminpanel/lib/media-manager/AbstractMediaManager"); +const MediaManagerHelper_1 = require("sails-adminpanel/lib/media-manager/helpers/MediaManagerHelper"); +const image_size_1 = __importDefault(require("image-size")); +const sharp = __importStar(require("sharp")); +class ImageItem extends AbstractMediaManager_1.File { + constructor(path, dir, model, metaModel) { + super(path, dir, model, metaModel); + this.type = "image"; + } + async getItems(limit, skip, sort) { + let data = await sails.models[this.model].find({ + where: { parent: null, mimeType: { contains: this.type } }, + limit: limit, + skip: skip, + sort: sort //@ts-ignore + }).populate('children', { sort: sort }); + let next = (await sails.models[this.model].find({ + where: { parent: null, mimeType: { contains: this.type } }, + limit: limit, + skip: skip === 0 ? limit : skip + limit, + sort: sort + })).length; + return { + data: data, + next: !!next + }; + } + async search(s) { + return await sails.models[this.model].find({ + where: { filename: { contains: s }, mimeType: { contains: this.type }, parent: null }, + sort: 'createdAt DESC' + }).populate('children', { sort: 'createdAt DESC' }); + } + async upload(file, filename, origFileName, imageSizes) { + let parent = await sails.models[this.model].create({ + parent: null, + mimeType: file.type, + size: file.size, + path: file.fd, + cropType: 'origin', + filename: origFileName, + image_size: (0, image_size_1.default)(file.fd), + url: `/${this.path}/${filename}` + }).fetch(); + await this.createEmptyMeta(parent.id); + if (parent.image_size.width > 150 && parent.image_size.height > 150) { + await this.createThumb(parent.id, file, filename, origFileName); + } + if (Object.keys(imageSizes).length) { + await this.createSizes(file, parent, filename, imageSizes); + } + return sails.models[this.model].find({ + where: { id: parent.id } + }).populate('children'); + } + async getChildren(id) { + return (await sails.models[this.model].findOne({ + where: { id: id } + }).populate('children', { sort: 'createdAt DESC' })).children; + } + async createSizes(file, parent, filename, imageSizes) { + for (const sizeKey of Object.keys(imageSizes)) { + let sizeName = (0, MediaManagerHelper_1.randomFileName)(filename, sizeKey, false); + let { width, height } = imageSizes[sizeKey]; + if (parent.image_size.width < width || parent.image_size.height < height) + continue; + let newFile = await this.resizeImage(file.fd, `${this.dir}${sizeName}`, width, height); + await sails.models[this.model].create({ + parent: parent.id, + mimeType: parent.mimeType, + size: newFile.size, + filename: parent.filename, + path: `${this.dir}${sizeName}`, + cropType: sizeKey, + image_size: (0, image_size_1.default)(`${this.dir}${sizeName}`), + url: `/${this.path}/${sizeName}` + }); + } + } + async createThumb(id, file, filename, origFileName) { + const thumbName = (0, MediaManagerHelper_1.randomFileName)(filename, 'thumb', false); + const thumb = await this.resizeImage(file.fd, `${this.dir}${thumbName}`, 150, 150); + await sails.models[this.model].create({ + parent: id, + mimeType: file.type, + size: thumb.size, + cropType: 'thumb', + path: `${this.dir}${thumbName}`, + filename: origFileName, + image_size: (0, image_size_1.default)(`${this.dir}${thumbName}`), + url: `/${this.path}/${thumbName}` + }); + } + async createEmptyMeta(id) { + //create empty meta + let metaData = { + author: "", + description: "", + title: "" + }; + for (const key of Object.keys(metaData)) { + await sails.models[this.metaModel].create({ + key: key, + value: metaData[key], + parent: id + }); + } + } + async getMeta(id) { + return (await sails.models[this.model].findOne(id).populate('meta')).meta; + } + async setMeta(id, data) { + for (const key of Object.keys(data)) { + await sails.models[this.metaModel].update({ parent: id, key: key }, { value: data[key] }); + } + return { msg: "success" }; + } + async resizeImage(input, output, width, height) { + return await sharp(input) + .resize({ width: width, height: height }) + .toFile(output); + } + async uploadCropped(parent, file, filename, config) { + return await sails.models[this.model].create({ + parent: parent.id, + mimeType: file.type, + size: file.size, + path: file.fd, + cropType: `${config.width}x${config.height}`, + filename: parent.filename, + image_size: (0, image_size_1.default)(file.fd), + url: `/${this.path}/${filename}` + }).fetch(); + } + async delete(id) { + await sails.models[this.model].destroy({ where: { id: id } }).fetch(); + } +} +exports.ImageItem = ImageItem; diff --git a/libs/adminpanel/ProductMediaManager/Item.ts b/libs/adminpanel/ProductMediaManager/Item.ts new file mode 100644 index 00000000..ca075184 --- /dev/null +++ b/libs/adminpanel/ProductMediaManager/Item.ts @@ -0,0 +1,170 @@ +import {File, Item, UploaderFile, imageSizes} from 'sails-adminpanel/lib/media-manager/AbstractMediaManager' +import {randomFileName} from "sails-adminpanel/lib/media-manager/helpers/MediaManagerHelper"; +import sizeOf from "image-size"; +import sharp from "sharp"; + +interface Meta { + [key: string]: string +} + +export class ImageItem extends File { + public type: "application" | "audio" | "example" | "image" | "message" | "model" | "multipart" | "text" | "video" = "image"; + + constructor(path: string, dir: string, model: string, metaModel: string) { + super(path, dir, model, metaModel); + } + + public async getItems(limit: number, skip: number, sort: string): Promise<{ data: Item[]; next: boolean }> { + let data: Item[] = await sails.models[this.model].find({ + where: {parent: null, mimeType: { contains: this.type}}, + limit: limit, + skip: skip, + sort: sort//@ts-ignore + }).populate('children', {sort: sort}) + + let next: number = (await sails.models[this.model].find({ + where: {parent: null, mimeType: { contains: this.type}}, + limit: limit, + skip: skip === 0 ? limit : skip + limit, + sort: sort + })).length + + return { + data: data, + next: !!next + } + } + + public async search(s: string): Promise{ + return await sails.models[this.model].find({ + where: {filename: {contains: s}, mimeType: { contains: this.type}, parent: null}, + sort: 'createdAt DESC' + }).populate('children', {sort: 'createdAt DESC'}) + } + + public async upload(file: UploaderFile, filename: string, origFileName: string, imageSizes?: imageSizes | {}): Promise { + + let parent: Item = await sails.models[this.model].create({ + parent: null, + mimeType: file.type, + size: file.size, + path: file.fd, + cropType: 'origin', + filename: origFileName, + image_size: sizeOf(file.fd), + url: `/${this.path}/${filename}` + }).fetch(); + + await this.createEmptyMeta(parent.id) + + if (parent.image_size.width > 150 && parent.image_size.height > 150) { + await this.createThumb(parent.id, file, filename, origFileName) + } + + if (Object.keys(imageSizes).length) { + await this.createSizes(file, parent, filename, imageSizes) + } + + return sails.models[this.model].find({ + where: {id: parent.id} + }).populate('children') as Item; + } + + public async getChildren(id: string): Promise { + return (await sails.models[this.model].findOne({ + where: {id: id} + }).populate('children', {sort: 'createdAt DESC'})).children + } + + protected async createSizes(file: UploaderFile, parent: Item, filename: string, imageSizes: imageSizes): Promise { + for (const sizeKey of Object.keys(imageSizes)) { + let sizeName = randomFileName(filename, sizeKey, false) + + let {width, height} = imageSizes[sizeKey] + + if (parent.image_size.width < width || parent.image_size.height < height) continue + + let newFile = await this.resizeImage(file.fd, `${this.dir}${sizeName}`, width, height) + await sails.models[this.model].create({ + parent: parent.id, + mimeType: parent.mimeType, + size: newFile.size, + filename: parent.filename, + path: `${this.dir}${sizeName}`, + cropType: sizeKey, + image_size: sizeOf(`${this.dir}${sizeName}`), + url: `/${this.path}/${sizeName}` + }) + } + } + + protected async createThumb(id: string, file: UploaderFile, filename: string, origFileName: string): Promise { + const thumbName = randomFileName(filename, 'thumb', false) + const thumb = await this.resizeImage(file.fd, `${this.dir}${thumbName}`, 150, 150) + + await sails.models[this.model].create({ + parent: id, + mimeType: file.type, + size: thumb.size, + cropType: 'thumb', + path: `${this.dir}${thumbName}`, + filename: origFileName, + image_size: sizeOf(`${this.dir}${thumbName}`), + url: `/${this.path}/${thumbName}` + }) + } + + protected async createEmptyMeta(id: string) { + //create empty meta + let metaData: Meta = { + author: "", + description: "", + title: "" + } + + for (const key of Object.keys(metaData)) { + await sails.models[this.metaModel].create({ + key: key, + value: metaData[key], + parent: id + }) + } + } + + public async getMeta(id: string): Promise<{ key: string, value: string }[]> { + return (await sails.models[this.model].findOne(id).populate('meta')).meta + } + + async setMeta(id: string, data: { [p: string]: string }): Promise<{ msg: "success" }> { + for (const key of Object.keys(data)) { + await sails.models[this.metaModel].update({parent: id, key: key}, {value: data[key]}) + } + return {msg: "success"} + } + + protected async resizeImage(input: string, output: string, width: number, height: number) { + return await sharp(input) + .resize({width: width, height: height}) + .toFile(output) + } + + public async uploadCropped(parent: Item, file: UploaderFile, filename: string, config: { + width: number, + height: number + }): Promise { + return await sails.models[this.model].create({ + parent: parent.id, + mimeType: file.type, + size: file.size, + path: file.fd, + cropType: `${config.width}x${config.height}`, + filename: parent.filename, + image_size: sizeOf(file.fd), + url: `/${this.path}/${filename}` + }).fetch() + } + + async delete(id: string): Promise { + await sails.models[this.model].destroy({where: {id: id}}).fetch() + } +} \ No newline at end of file diff --git a/libs/adminpanel/ProductMediaManager/ProductMediaManager.d.ts b/libs/adminpanel/ProductMediaManager/ProductMediaManager.d.ts new file mode 100644 index 00000000..40e259d9 --- /dev/null +++ b/libs/adminpanel/ProductMediaManager/ProductMediaManager.d.ts @@ -0,0 +1,14 @@ +import { AbstractMediaManager, Item, File, WidgetItem, Data } from "./AbstractMediaManager"; +export declare class DefaultMediaManager extends AbstractMediaManager { + readonly itemTypes: File[]; + constructor(id: string, path: string, dir: string, model: string, metaModel: string, modelAssoc: string); + getAll(limit: number, skip: number, sort: string): Promise<{ + data: Item[]; + next: boolean; + }>; + searchAll(s: string): Promise; + saveRelations(data: Data, model: string, modelId: string, modelAttribute: string): Promise; + getRelations(items: WidgetItem[]): Promise; + updateRelations(data: Data, model: string, modelId: string, modelAttribute: string): Promise; + deleteRelations(model: string, modelId: string): Promise; +} diff --git a/libs/adminpanel/ProductMediaManager/ProductMediaManager.js b/libs/adminpanel/ProductMediaManager/ProductMediaManager.js new file mode 100644 index 00000000..f2dd405a --- /dev/null +++ b/libs/adminpanel/ProductMediaManager/ProductMediaManager.js @@ -0,0 +1,80 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DefaultMediaManager = void 0; +const AbstractMediaManager_1 = require("./AbstractMediaManager"); +const Items_1 = require("./Items"); +class DefaultMediaManager extends AbstractMediaManager_1.AbstractMediaManager { + constructor(id, path, dir, model, metaModel, modelAssoc) { + super(id, path, dir, model, modelAssoc); + this.itemTypes = []; + this.itemTypes.push(new Items_1.ImageItem(path, dir, model, metaModel)); + this.itemTypes.push(new Items_1.TextItem(path, dir, model, metaModel)); + this.itemTypes.push(new Items_1.ApplicationItem(path, dir, model, metaModel)); + this.itemTypes.push(new Items_1.VideoItem(path, dir, model, metaModel)); + } + async getAll(limit, skip, sort) { + let data = await sails.models[this.model].find({ + where: { parent: null }, + limit: limit, + skip: skip, + sort: sort //@ts-ignore + }).populate('children', { sort: sort }); + let next = (await sails.models[this.model].find({ + where: { parent: null }, + limit: limit, + skip: skip === 0 ? limit : skip + limit, + sort: sort + })).length; + return { + data: data, + next: !!next + }; + } + async searchAll(s) { + return await sails.models[this.model].find({ + where: { filename: { contains: s }, parent: null }, + sort: 'createdAt DESC' + }).populate('children', { sort: 'createdAt DESC' }); + } + async saveRelations(data, model, modelId, modelAttribute) { + let widgetItems = []; + for (const [key, widgetItem] of data.list.entries()) { + let record = await sails.models[this.modelAssoc].create({ + mediaManagerId: this.id, + model: model, + modelId: modelId, + file: widgetItem.id, + sortOrder: key + 1, + }).fetch(); + widgetItems.push({ + id: record.id, + }); + } + let updateData = {}; + updateData[modelAttribute] = { list: widgetItems, mediaManagerId: this.id }; + await sails.models[model].update({ id: modelId }, updateData); + } + async getRelations(items) { + let widgetItems = []; + for (const item of items) { + let record = (await sails.models[this.modelAssoc].find({ where: { id: item.id } }))[0]; + let file = (await sails.models[this.model].find({ where: { id: record.file } }).populate('children', { sort: 'createdAt DESC' }))[0]; + widgetItems.push({ + id: file.id, + children: file.children, + }); + } + return widgetItems; + } + async updateRelations(data, model, modelId, modelAttribute) { + await this.deleteRelations(model, modelId); + await this.saveRelations(data, model, modelId, modelAttribute); + } + async deleteRelations(model, modelId) { + let modelAssociations = await sails.models[this.modelAssoc].find({ where: { modelId: modelId, model: model } }); + for (const modelAssociation of modelAssociations) { + await sails.models[this.modelAssoc].destroy(modelAssociation.id).fetch(); + } + } +} +exports.DefaultMediaManager = DefaultMediaManager; diff --git a/libs/adminpanel/ProductMediaManager/ProductMediaManager.ts b/libs/adminpanel/ProductMediaManager/ProductMediaManager.ts new file mode 100644 index 00000000..4bf5da47 --- /dev/null +++ b/libs/adminpanel/ProductMediaManager/ProductMediaManager.ts @@ -0,0 +1,92 @@ +import {AbstractMediaManager, Item, File, WidgetItem, Data, widgetJSON} from "./AbstractMediaManager"; +import {ApplicationItem, ImageItem, TextItem, VideoItem} from "./Items"; + +export class DefaultMediaManager extends AbstractMediaManager { + public readonly itemTypes: File[] = []; + + constructor(id: string, path: string, dir: string, model: string, metaModel: string, modelAssoc: string) { + super(id, path, dir, model, modelAssoc); + this.itemTypes.push(new ImageItem(path, dir, model, metaModel)) + this.itemTypes.push(new TextItem(path, dir, model, metaModel)) + this.itemTypes.push(new ApplicationItem(path, dir, model, metaModel)) + this.itemTypes.push(new VideoItem(path, dir, model, metaModel)) + } + + public async getAll(limit: number, skip: number, sort: string): Promise<{ data: Item[], next: boolean }> { + let data: Item[] = await sails.models[this.model].find({ + where: {parent: null}, + limit: limit, + skip: skip, + sort: sort//@ts-ignore + }).populate('children', {sort: sort}) + + let next: number = (await sails.models[this.model].find({ + where: {parent: null}, + limit: limit, + skip: skip === 0 ? limit : skip + limit, + sort: sort + })).length + + return { + data: data, + next: !!next + } + } + + public async searchAll(s: string): Promise { + return await sails.models[this.model].find({ + where: {filename: {contains: s}, parent: null}, + sort: 'createdAt DESC' + }).populate('children', {sort: 'createdAt DESC'}) + } + + public async saveRelations(data: Data, model: string, modelId: string, modelAttribute: string): Promise { + let widgetItems: WidgetItem[] = [] + for (const [key, widgetItem] of data.list.entries()) { + let record = await sails.models[this.modelAssoc].create({ + mediaManagerId: this.id, + model: model, + modelId: modelId, + file: widgetItem.id, + sortOrder: key + 1, + }).fetch() + widgetItems.push({ + id: record.id as string, + }) + } + + let updateData: { [key: string]: widgetJSON } = {} + + updateData[modelAttribute] = {list: widgetItems, mediaManagerId: this.id} + + await sails.models[model].update({id: modelId}, updateData) + } + + public async getRelations(items: WidgetItem[]): Promise { + interface widgetItemVUE extends WidgetItem { + children: Item[] + } + let widgetItems: widgetItemVUE[] = [] + for (const item of items) { + let record = (await sails.models[this.modelAssoc].find({where: {id: item.id}}))[0] + let file = (await sails.models[this.model].find({where: {id: record.file}}).populate('children', {sort: 'createdAt DESC'}))[0] as Item + widgetItems.push({ + id: file.id, + children: file.children, + }) + } + return widgetItems + } + + public async updateRelations(data: Data, model: string, modelId: string, modelAttribute: string): Promise{ + await this.deleteRelations(model, modelId) + await this.saveRelations(data, model, modelId, modelAttribute) + } + + public async deleteRelations(model: string, modelId: string): Promise{ + let modelAssociations = await sails.models[this.modelAssoc].find({where: {modelId: modelId, model: model}}) + for (const modelAssociation of modelAssociations) { + await sails.models[this.modelAssoc].destroy(modelAssociation.id).fetch() + } + } +} diff --git a/libs/adminpanel/models/bind.d.ts b/libs/adminpanel/models/bind.d.ts index b8e119b0..8b98bd76 100644 --- a/libs/adminpanel/models/bind.d.ts +++ b/libs/adminpanel/models/bind.d.ts @@ -8,8 +8,11 @@ declare const _default: { model: string; title: string; icon: string; - list: {}; - edit: {}; + list: { + fields: import("sails-adminpanel/interfaces/adminpanelConfig").FieldsModels; + }; + edit: import("sails-adminpanel/interfaces/adminpanelConfig").CreateUpdateConfig; + add: import("sails-adminpanel/interfaces/adminpanelConfig").CreateUpdateConfig; }; groups: { model: string; diff --git a/libs/adminpanel/models/bind.js b/libs/adminpanel/models/bind.js index 32d8ad68..e61e6e77 100644 --- a/libs/adminpanel/models/bind.js +++ b/libs/adminpanel/models/bind.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +const product_1 = require("./lib/product"); exports.default = { user: { title: "User", @@ -10,8 +11,9 @@ exports.default = { model: 'dish', title: 'Products', icon: 'hamburger', - list: {}, - edit: {} + list: product_1.ProductConfig.list(), + edit: product_1.ProductConfig.edit(), + add: product_1.ProductConfig.add(), }, groups: { model: 'group', diff --git a/libs/adminpanel/models/lib/product.d.ts b/libs/adminpanel/models/lib/product.d.ts index 469cad16..95884de4 100644 --- a/libs/adminpanel/models/lib/product.d.ts +++ b/libs/adminpanel/models/lib/product.d.ts @@ -1,4 +1,9 @@ -declare class ProductConfig { - static add(): {}; - static edit(): {}; +import { CreateUpdateConfig, FieldsModels } from "sails-adminpanel/interfaces/adminpanelConfig"; +export declare class ProductConfig { + static fields: FieldsModels; + static add(): CreateUpdateConfig; + static edit(): CreateUpdateConfig; + static list(): { + fields: FieldsModels; + }; } diff --git a/libs/adminpanel/models/lib/product.js b/libs/adminpanel/models/lib/product.js index 049702c9..115a32f1 100644 --- a/libs/adminpanel/models/lib/product.js +++ b/libs/adminpanel/models/lib/product.js @@ -1,9 +1,180 @@ "use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ProductConfig = void 0; class ProductConfig { static add() { - return {}; + return { fields: this.fields }; } static edit() { - return {}; + return { fields: this.fields }; + } + static list() { + return { + fields: {} + }; } } +exports.ProductConfig = ProductConfig; +ProductConfig.fields = { + id: { + title: "Id", + disabled: true + }, + rmsId: { + title: "External Id", + disabled: true + }, + code: { + title: "SKU (code)", + }, + description: { + title: "Description", + type: "text" + }, + ingredients: { + title: "Ingredients" + }, + carbohydrateAmount: { + title: "Carbohydrate Amount (per unit volume)", + tooltip: "The amount of carbohydrates per unit volume of the product." + }, + carbohydrateFullAmount: { + title: "Total Carbohydrate Amount", + tooltip: "The total amount of carbohydrates in the entire product." + }, + energyAmount: { + title: "Energy Amount (per unit volume)", + tooltip: "The amount of energy (calories) per unit volume of the product." + }, + energyFullAmount: { + title: "Total Energy Amount", + tooltip: "The total amount of energy (calories) in the entire product." + }, + fatAmount: { + title: "Fat Amount (per unit volume)", + tooltip: "The amount of fat per unit volume of the product." + }, + fatFullAmount: { + title: "Total Fat Amount", + tooltip: "The total amount of fat in the entire product." + }, + fiberAmount: { + title: "Fiber Amount (per unit volume)", + tooltip: "The amount of dietary fiber per unit volume of the product." + }, + fiberFullAmount: { + title: "Total Fiber Amount", + tooltip: "The total amount of dietary fiber in the entire product." + }, + proteinAmount: { + title: "Protein Amount (per unit volume)", + tooltip: "The amount of protein per unit volume of the product." + }, + proteinFullAmount: { + title: "Total Protein Amount", + tooltip: "The total amount of protein in the entire product." + }, + measureUnit: { + title: "Measurement Unit", + tooltip: "The unit of measurement for this value, ml, kg, pcs ... ." + }, + type: { + title: "Type", + tooltip: "Dish, Service, Product, Modifier" + }, + weight: { + title: "weight", + tooltip: "Weight is measured in grams 1000 = 1 kilogram" + }, + sortOrder: { + title: "Sorting order", + tooltip: "Sorting is applied within a group" + }, + isDeleted: { + title: "Is Deleted", + tooltip: "Indicates whether the item is soft-deleted. If true, the item is not shown anywhere in the application and is considered removed." + }, + isModifiable: { + title: "Is Modifiable", + tooltip: "Indicates whether the product or dish can be modified through the modifier system. If true, the item can be adjusted or customized based on available modifiers." + }, + parentGroup: { + title: "Parent Group", + tooltip: "The group or category to which the dish belongs. This indicates the hierarchical relationship of the dish within its parent group or category." + }, + tags: { + title: "Tags", + type: "json", + tooltip: "A list of tags that can be applied to a dish or group. Tags help categorize and identify dishes based on attributes such as 'sweet', 'sour', 'salty', 'fried', 'boiled', 'hearty', 'fish', 'chicken', 'vegetarian', etc., making it easier to navigate the menu." + }, + balance: { + title: "Balance", + tooltip: "Indicates the availability of the dish based on its balance. If the balance is 0, the dish is no longer available for sale. If the balance is -1, the dish is available for sale with no restrictions." + }, + slug: { + title: "Slug", + tooltip: "A URL-friendly version of the dish's name. It is used to create a readable and unique identifier for the dish in the URL, typically consisting of lowercase letters, numbers, and hyphens." + }, + hash: false, + concept: { + title: "Concept", + tooltip: "The concept represents a broad category or section that distinguishes dishes and menus. It can be used to define different sections such as 'Burgers', 'Pizza', 'Kids Menu', or even separate restaurants operating under the same backend but with different frontends. Concepts can also be used to manage delivery from different regions, making it a versatile and abstract categorization tool." + }, + visible: { + title: "Visible", + tooltip: "Indicates whether the dish is visible to users. If true, the dish will be displayed in the menu; if false, it will be hidden." + }, + modifier: { + title: "Modifier", + tooltip: "Indicates whether the dish is a modifier. A modifier is a dish that can alter or customize other dishes, often used to add options or variations." + }, + promo: { + title: "Promo", + tooltip: "Indicates whether the dish is part of a promotional offer. If true, the dish is featured as part of a special promotion or discount." + }, + worktime: { + title: "Work Time", + tooltip: "Specifies the operational hours during which the dish is available. This can include specific time ranges or days when the dish is offered." + }, + modifiers: { + title: "Modifiers", + tooltip: "A list of modifiers applicable to the dish. Modifiers allow customization or variations of the dish, such as extra toppings or ingredient changes." + }, + images: { + title: "Images", + type: 'mediamanager', + options: { + config: { + convert: 'image/jpeg', // only 'image/webp' or 'image/jpeg' + sizes: [ + { + lg: { + width: 750, + height: 750 + } + }, + { + sm: { + width: 350, + height: 350 + } + }, + ] + } + }, + tooltip: "A list of images associated with the dish. These images are used to visually represent the dish and can be selected for display in the menu." + }, + favorites: false, + recommendations: { + title: "Recommendations", + tooltip: "A list of dishes recommended based on this dish. It helps suggest similar or complementary items to users." + }, + recommendedBy: { + title: "Recommended By", + tooltip: "A self-referential link that connects this dish to other dishes as recommendations. It indicates that this dish recommends or is recommended by others." + }, + customData: { + title: "Custom Data", + tooltip: "Additional fields or data that can be set by modules or custom configurations. This allows for flexible extensions and metadata specific to the dish." + } +}; diff --git a/libs/adminpanel/models/lib/product.ts b/libs/adminpanel/models/lib/product.ts index b68a23c9..ba8842b3 100644 --- a/libs/adminpanel/models/lib/product.ts +++ b/libs/adminpanel/models/lib/product.ts @@ -1,13 +1,175 @@ import { CreateUpdateConfig, FieldsModels } from "sails-adminpanel/interfaces/adminpanelConfig" export class ProductConfig { - public fields: FieldsModels; + static fields: FieldsModels = { + id: { + title: "Id", + disabled: true + }, + rmsId: { + title: "External Id", + disabled: true + }, + code: { + title: "SKU (code)", + }, + description: { + title: "Description", + type: "text" + }, + ingredients: { + title: "Ingredients" + }, + carbohydrateAmount: { + title: "Carbohydrate Amount (per unit volume)", + tooltip: "The amount of carbohydrates per unit volume of the product." + }, + carbohydrateFullAmount: { + title: "Total Carbohydrate Amount", + tooltip: "The total amount of carbohydrates in the entire product." + }, + energyAmount: { + title: "Energy Amount (per unit volume)", + tooltip: "The amount of energy (calories) per unit volume of the product." + }, + energyFullAmount: { + title: "Total Energy Amount", + tooltip: "The total amount of energy (calories) in the entire product." + }, + fatAmount: { + title: "Fat Amount (per unit volume)", + tooltip: "The amount of fat per unit volume of the product." + }, + fatFullAmount: { + title: "Total Fat Amount", + tooltip: "The total amount of fat in the entire product." + }, + fiberAmount: { + title: "Fiber Amount (per unit volume)", + tooltip: "The amount of dietary fiber per unit volume of the product." + }, + fiberFullAmount: { + title: "Total Fiber Amount", + tooltip: "The total amount of dietary fiber in the entire product." + }, + proteinAmount: { + title: "Protein Amount (per unit volume)", + tooltip: "The amount of protein per unit volume of the product." + }, + proteinFullAmount: { + title: "Total Protein Amount", + tooltip: "The total amount of protein in the entire product." + }, + measureUnit: { + title: "Measurement Unit", + tooltip: "The unit of measurement for this value, ml, kg, pcs ... ." + }, + type: { + title: "Type", + tooltip: "Dish, Service, Product, Modifier" + }, + weight: { + title: "weight", + tooltip: "Weight is measured in grams 1000 = 1 kilogram" + }, + sortOrder: { + title: "Sorting order", + tooltip: "Sorting is applied within a group" + }, + isDeleted: { + title: "Is Deleted", + tooltip: "Indicates whether the item is soft-deleted. If true, the item is not shown anywhere in the application and is considered removed." + }, + isModifiable: { + title: "Is Modifiable", + tooltip: "Indicates whether the product or dish can be modified through the modifier system. If true, the item can be adjusted or customized based on available modifiers." + }, + parentGroup: { + title: "Parent Group", + tooltip: "The group or category to which the dish belongs. This indicates the hierarchical relationship of the dish within its parent group or category." + }, + tags: { + title: "Tags", + type: "json", + tooltip: "A list of tags that can be applied to a dish or group. Tags help categorize and identify dishes based on attributes such as 'sweet', 'sour', 'salty', 'fried', 'boiled', 'hearty', 'fish', 'chicken', 'vegetarian', etc., making it easier to navigate the menu." + }, + balance: { + title: "Balance", + tooltip: "Indicates the availability of the dish based on its balance. If the balance is 0, the dish is no longer available for sale. If the balance is -1, the dish is available for sale with no restrictions." + }, + slug: { + title: "Slug", + tooltip: "A URL-friendly version of the dish's name. It is used to create a readable and unique identifier for the dish in the URL, typically consisting of lowercase letters, numbers, and hyphens." + }, + hash: false, + concept: { + title: "Concept", + tooltip: "The concept represents a broad category or section that distinguishes dishes and menus. It can be used to define different sections such as 'Burgers', 'Pizza', 'Kids Menu', or even separate restaurants operating under the same backend but with different frontends. Concepts can also be used to manage delivery from different regions, making it a versatile and abstract categorization tool." + }, + visible: { + title: "Visible", + tooltip: "Indicates whether the dish is visible to users. If true, the dish will be displayed in the menu; if false, it will be hidden." + }, + modifier: { + title: "Modifier", + tooltip: "Indicates whether the dish is a modifier. A modifier is a dish that can alter or customize other dishes, often used to add options or variations." + }, + promo: { + title: "Promo", + tooltip: "Indicates whether the dish is part of a promotional offer. If true, the dish is featured as part of a special promotion or discount." + }, + worktime: { + title: "Work Time", + tooltip: "Specifies the operational hours during which the dish is available. This can include specific time ranges or days when the dish is offered." + }, + modifiers: { + title: "Modifiers", + tooltip: "A list of modifiers applicable to the dish. Modifiers allow customization or variations of the dish, such as extra toppings or ingredient changes." + }, + images: { + title: "Images", + type: 'mediamanager', + options: { + config: { + convert: 'image/jpeg', // only 'image/webp' or 'image/jpeg' + sizes: [ + { + lg: { + width: 750, + height: 750 + } + }, + { + sm: { + width: 350, + height: 350 + } + }, + ] + } + }, + tooltip: "A list of images associated with the dish. These images are used to visually represent the dish and can be selected for display in the menu." + }, + favorites: false, + recommendations: { + title: "Recommendations", + tooltip: "A list of dishes recommended based on this dish. It helps suggest similar or complementary items to users." + }, + recommendedBy: { + title: "Recommended By", + tooltip: "A self-referential link that connects this dish to other dishes as recommendations. It indicates that this dish recommends or is recommended by others." + }, + customData: { + title: "Custom Data", + tooltip: "Additional fields or data that can be set by modules or custom configurations. This allows for flexible extensions and metadata specific to the dish." + } + }; public static add(): CreateUpdateConfig { - return {} + return { fields: this.fields }; } public static edit(): CreateUpdateConfig { - return {} + return { fields: this.fields }; } public static list(): {fields: FieldsModels} { diff --git a/models/MediaFile.d.ts b/models/MediaFile.d.ts index fd03a719..0525a76d 100644 --- a/models/MediaFile.d.ts +++ b/models/MediaFile.d.ts @@ -14,7 +14,10 @@ declare let attributes: { original: string; /** Dish relation */ dish: Dish[]; - /** Sort order */ + /** + * Sort order + * @deprecated was moved to junction table + * */ sortOrder: number; /** Group relation */ group: Group[]; @@ -26,7 +29,9 @@ declare let attributes: { type attributes = typeof attributes; interface MediaFile extends OptionalAll, ORM { } +/** @deprecated use `MediaFileRecord` */ export type IMediaFile = OptionalAll; +export type MediaFileRecord = OptionalAll; declare let Model: { beforeCreate(imageInit: any, cb: (err?: string) => void): void; afterDestroy(mf: MediaFile, cb: (err?: any) => void): Promise; diff --git a/models/MediaFile.js b/models/MediaFile.js index 4f0b74b4..33ad0a69 100644 --- a/models/MediaFile.js +++ b/models/MediaFile.js @@ -47,7 +47,10 @@ let attributes = { collection: "dish", via: "images", }, - /** Sort order */ + /** + * Sort order + * @deprecated was moved to junction table + * */ sortOrder: "number", /** Group relation */ group: { diff --git a/models/MediaFile.ts b/models/MediaFile.ts index 2678009c..a81cce1a 100644 --- a/models/MediaFile.ts +++ b/models/MediaFile.ts @@ -35,7 +35,10 @@ let attributes = { via: "images", } as unknown as Dish[], - /** Sort order */ + /** + * Sort order + * @deprecated was moved to junction table + * */ sortOrder: "number" as unknown as number, /** Group relation */ @@ -51,7 +54,10 @@ let attributes = { }; type attributes = typeof attributes; interface MediaFile extends OptionalAll, ORM { } +/** @deprecated use `MediaFileRecord` */ export type IMediaFile = OptionalAll; +export type MediaFileRecord = OptionalAll; + // export default IMediaFile; let Model = { diff --git a/models/SelectedMediaFile.d.ts b/models/SelectedMediaFile.d.ts new file mode 100644 index 00000000..9a581765 --- /dev/null +++ b/models/SelectedMediaFile.d.ts @@ -0,0 +1,25 @@ +import ORM from "../interfaces/ORM"; +import { ORMModel } from "../interfaces/ORMModel"; +import { OptionalAll } from "../interfaces/toolsTS"; +import { MediaFileRecord } from "./MediaFile"; +declare let attributes: { + /** ID */ + id: string; + /** + * Sort order + * */ + sortOrder: number; + /** MediaFile reference */ + mediaFile: MediaFileRecord; +}; +type attributes = typeof attributes; +interface SelectedMediaFile extends OptionalAll, ORM { +} +export type SelectedMediaFileRecord = OptionalAll; +declare let Model: { + beforeCreate(imageInit: SelectedMediaFileRecord, cb: (err?: string) => void): void; +}; +declare global { + const SelectedMediaFile: typeof Model & ORMModel; +} +export {}; diff --git a/models/SelectedMediaFile.js b/models/SelectedMediaFile.js new file mode 100644 index 00000000..4df096bb --- /dev/null +++ b/models/SelectedMediaFile.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const uuid_1 = require("uuid"); +let attributes = { + /** ID */ + id: { + type: "string", + //required: true, + }, + /** + * Sort order + * */ + sortOrder: "number", + /** MediaFile reference */ + mediaFile: { + model: 'mediafile', + required: true, + }, +}; +let Model = { + beforeCreate(imageInit, cb) { + if (!imageInit.id) { + imageInit.id = (0, uuid_1.v4)(); + } + cb(); + } +}; +module.exports = { + primaryKey: "id", + attributes: attributes, + ...Model, +}; diff --git a/models/SelectedMediaFile.ts b/models/SelectedMediaFile.ts new file mode 100644 index 00000000..d3c8d016 --- /dev/null +++ b/models/SelectedMediaFile.ts @@ -0,0 +1,48 @@ +import ORM from "../interfaces/ORM"; +import { ORMModel } from "../interfaces/ORMModel"; +import { v4 as uuid } from "uuid"; +import { OptionalAll } from "../interfaces/toolsTS"; +import { MediaFileRecord } from "./MediaFile"; + +let attributes = { + /** ID */ + id: { + type: "string", + //required: true, + } as unknown as string, + + /** + * Sort order + * */ + sortOrder: "number" as unknown as number, + + /** MediaFile reference */ + mediaFile: { + model: 'mediafile', + required: true, + } as unknown as MediaFileRecord, +}; + +type attributes = typeof attributes; +interface SelectedMediaFile extends OptionalAll, ORM { } +export type SelectedMediaFileRecord = OptionalAll; + +let Model = { + beforeCreate(imageInit: SelectedMediaFileRecord, cb: (err?: string) => void) { + if (!imageInit.id) { + imageInit.id = uuid(); + } + + cb(); + } +}; + +module.exports = { + primaryKey: "id", + attributes: attributes, + ...Model, +}; + +declare global { + const SelectedMediaFile: typeof Model & ORMModel; +} diff --git a/package.json b/package.json index 4bb241a7..6230a3d1 100644 --- a/package.json +++ b/package.json @@ -35,31 +35,31 @@ "devDependencies": { "@42pub/typed-sails": "^1.0.1", "@eslint/compat": "^1.1.1", - "@eslint/js": "^9.8.0", + "@eslint/js": "^9.9.1", "@types/bcryptjs": "^2.4.6", "@types/bluebird": "^3.5.42", "@types/bluebird-global": "^3.5.18", "@types/body-parser": "^1.19.5", - "@types/chai": "^4.3.17", + "@types/chai": "^4.3.19", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", "@types/faker": "^4.1.12", "@types/lodash": "^4.17.7", "@types/mocha": "^8.0.2", - "@types/node": "^22.1.0", + "@types/node": "^22.5.3", "@types/uuid": "^10.0.0", - "@types/waterline": "^0.13.4", + "@types/waterline": "^0.13.9", "async": "^3.2.5", "chai": "^4.2.0", "dotenv": "^8.2.0", - "eslint": "^9.8.0", + "eslint": "^9.9.1", "faker": "^4.1.0", "intermock": "^0.2.5", "mocha": "^8.1.1", - "sails-adminpanel": "3.2.0-build.1", + "sails-adminpanel": "3.2.0-build.18", "ts-node": "^10.9.2", "typescript": "^5.5.4", - "typescript-eslint": "^8.0.1" + "typescript-eslint": "^8.4.0" }, "deprecated": false, "description": "Local e-commerce backend core", diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 00000000..54a70248 --- /dev/null +++ b/translations/en.json @@ -0,0 +1,67 @@ +{ + "proteinAmount": "proteinAmount", + "proteinFullAmount": "proteinFullAmount", + "recommendations": "recommendations", + "recommendedBy": "recommendedBy", + "dev-MediaManagerAssociationsAP": "dev-MediaManagerAssociationsAP", + "Id": "Id", + "External Id": "External Id", + "SKU (code)": "SKU (code)", + "Ingredients": "Ingredients", + "Carbohydrate Amount (per unit volume)": "Carbohydrate Amount (per unit volume)", + "The amount of carbohydrates per unit volume of the product.": "The amount of carbohydrates per unit volume of the product.", + "Total Carbohydrate Amount": "Total Carbohydrate Amount", + "The total amount of carbohydrates in the entire product.": "The total amount of carbohydrates in the entire product.", + "Energy Amount (per unit volume)": "Energy Amount (per unit volume)", + "The amount of energy (calories) per unit volume of the product.": "The amount of energy (calories) per unit volume of the product.", + "Total Energy Amount": "Total Energy Amount", + "The total amount of energy (calories) in the entire product.": "The total amount of energy (calories) in the entire product.", + "Fat Amount (per unit volume)": "Fat Amount (per unit volume)", + "The amount of fat per unit volume of the product.": "The amount of fat per unit volume of the product.", + "Total Fat Amount": "Total Fat Amount", + "The total amount of fat in the entire product.": "The total amount of fat in the entire product.", + "Fiber Amount (per unit volume)": "Fiber Amount (per unit volume)", + "The amount of dietary fiber per unit volume of the product.": "The amount of dietary fiber per unit volume of the product.", + "Total Fiber Amount": "Total Fiber Amount", + "The total amount of dietary fiber in the entire product.": "The total amount of dietary fiber in the entire product.", + "Protein Amount (per unit volume)": "Protein Amount (per unit volume)", + "The amount of protein per unit volume of the product.": "The amount of protein per unit volume of the product.", + "Total Protein Amount": "Total Protein Amount", + "The total amount of protein in the entire product.": "The total amount of protein in the entire product.", + "Measurement Unit": "Measurement Unit", + "The unit of measurement for this value, ml, kg, pcs ... .": "The unit of measurement for this value, ml, kg, pcs ... .", + "Dish, Service, Product, Modifier": "Dish, Service, Product, Modifier", + "Weight is measured in grams 1000 = 1 kilogram": "Weight is measured in grams 1000 = 1 kilogram", + "Sorting order": "Sorting order", + "Sorting is applied within a group": "Sorting is applied within a group", + "Is Deleted": "Is Deleted", + "Indicates whether the item is soft-deleted. If true, the item is not shown anywhere in the application and is considered removed.": "Indicates whether the item is soft-deleted. If true, the item is not shown anywhere in the application and is considered removed.", + "Parent Group": "Parent Group", + "The group or category to which the dish belongs. This indicates the hierarchical relationship of the dish within its parent group or category.": "The group or category to which the dish belongs. This indicates the hierarchical relationship of the dish within its parent group or category.", + "Tags": "Tags", + "A list of tags that can be applied to a dish or group. Tags help categorize and identify dishes based on attributes such as 'sweet', 'sour', 'salty', 'fried', 'boiled', 'hearty', 'fish', 'chicken', 'vegetarian', etc., making it easier to navigate the menu.": "A list of tags that can be applied to a dish or group. Tags help categorize and identify dishes based on attributes such as 'sweet', 'sour', 'salty', 'fried', 'boiled', 'hearty', 'fish', 'chicken', 'vegetarian', etc., making it easier to navigate the menu.", + "Balance": "Balance", + "Indicates the availability of the dish based on its balance. If the balance is 0, the dish is no longer available for sale. If the balance is -1, the dish is available for sale with no restrictions.": "Indicates the availability of the dish based on its balance. If the balance is 0, the dish is no longer available for sale. If the balance is -1, the dish is available for sale with no restrictions.", + "Slug": "Slug", + "A URL-friendly version of the dish's name. It is used to create a readable and unique identifier for the dish in the URL, typically consisting of lowercase letters, numbers, and hyphens.": "A URL-friendly version of the dish's name. It is used to create a readable and unique identifier for the dish in the URL, typically consisting of lowercase letters, numbers, and hyphens.", + "Concept": "Concept", + "The concept represents a broad category or section that distinguishes dishes and menus. It can be used to define different sections such as 'Burgers', 'Pizza', 'Kids Menu', or even separate restaurants operating under the same backend but with different frontends. Concepts can also be used to manage delivery from different regions, making it a versatile and abstract categorization tool.": "The concept represents a broad category or section that distinguishes dishes and menus. It can be used to define different sections such as 'Burgers', 'Pizza', 'Kids Menu', or even separate restaurants operating under the same backend but with different frontends. Concepts can also be used to manage delivery from different regions, making it a versatile and abstract categorization tool.", + "Visible": "Visible", + "Indicates whether the dish is visible to users. If true, the dish will be displayed in the menu; if false, it will be hidden.": "Indicates whether the dish is visible to users. If true, the dish will be displayed in the menu; if false, it will be hidden.", + "Modifier": "Modifier", + "Indicates whether the dish is a modifier. A modifier is a dish that can alter or customize other dishes, often used to add options or variations.": "Indicates whether the dish is a modifier. A modifier is a dish that can alter or customize other dishes, often used to add options or variations.", + "Promo": "Promo", + "Indicates whether the dish is part of a promotional offer. If true, the dish is featured as part of a special promotion or discount.": "Indicates whether the dish is part of a promotional offer. If true, the dish is featured as part of a special promotion or discount.", + "Work Time": "Work Time", + "Specifies the operational hours during which the dish is available. This can include specific time ranges or days when the dish is offered.": "Specifies the operational hours during which the dish is available. This can include specific time ranges or days when the dish is offered.", + "Modifiers": "Modifiers", + "A list of modifiers applicable to the dish. Modifiers allow customization or variations of the dish, such as extra toppings or ingredient changes.": "A list of modifiers applicable to the dish. Modifiers allow customization or variations of the dish, such as extra toppings or ingredient changes.", + "Images": "Images", + "A list of images associated with the dish. These images are used to visually represent the dish and can be selected for display in the menu.": "A list of images associated with the dish. These images are used to visually represent the dish and can be selected for display in the menu.", + "Recommendations": "Recommendations", + "A list of dishes recommended based on this dish. It helps suggest similar or complementary items to users.": "A list of dishes recommended based on this dish. It helps suggest similar or complementary items to users.", + "Recommended By": "Recommended By", + "A self-referential link that connects this dish to other dishes as recommendations. It indicates that this dish recommends or is recommended by others.": "A self-referential link that connects this dish to other dishes as recommendations. It indicates that this dish recommends or is recommended by others.", + "Custom Data": "Custom Data", + "Additional fields or data that can be set by modules or custom configurations. This allows for flexible extensions and metadata specific to the dish.": "Additional fields or data that can be set by modules or custom configurations. This allows for flexible extensions and metadata specific to the dish." +} \ No newline at end of file diff --git a/translations/ru.json b/translations/ru.json new file mode 100644 index 00000000..150d6603 --- /dev/null +++ b/translations/ru.json @@ -0,0 +1,66 @@ +{ + "proteinAmount": "Количество белка", + "proteinFullAmount": "Общее количество белка", + "recommendations": "Рекомендации", + "recommendedBy": "Рекомендовано", + "dev-MediaManagerAssociationsAP": "dev-MediaManagerAssociationsAP", + "Id": "Идентификатор", + "External Id": "Внешний идентификатор", + "SKU (code)": "SKU (код)", + "Ingredients": "Ингредиенты", + "Carbohydrate Amount (per unit volume)": "Количество углеводов (на единицу объема)", + "The amount of carbohydrates per unit volume of the product.": "Количество углеводов на единицу объема продукта.", + "Total Carbohydrate Amount": "Общее количество углеводов", + "The total amount of carbohydrates in the entire product.": "Общее количество углеводов в продукте.", + "Energy Amount (per unit volume)": "Количество энергии (на единицу объема)", + "The amount of energy (calories) per unit volume of the product.": "Количество энергии (калорий) на единицу объема продукта.", + "Total Energy Amount": "Общее количество энергии", + "The total amount of energy (calories) in the entire product.": "Общее количество энергии (калорий) в продукте.", + "Fat Amount (per unit volume)": "Количество жира (на единицу объема)", + "The amount of fat per unit volume of the product.": "Количество жира на единицу объема продукта.", + "Total Fat Amount": "Общее количество жира", + "The total amount of fat in the entire product.": "Общее количество жира в продукте.", + "Fiber Amount (per unit volume)": "Количество клетчатки (на единицу объема)", + "The amount of dietary fiber per unit volume of the product.": "Количество клетчатки на единицу объема продукта.", + "Total Fiber Amount": "Общее количество клетчатки", + "The total amount of dietary fiber in the entire product.": "Общее количество клетчатки в продукте.", + "Protein Amount (per unit volume)": "Количество белка (на единицу объема)", + "The amount of protein per unit volume of the product.": "Количество белка на единицу объема продукта.", + "Total Protein Amount": "Общее количество белка", + "The total amount of protein in the entire product.": "Общее количество белка в продукте.", + "Measurement Unit": "Единица измерения", + "The unit of measurement for this value, ml, kg, pcs ... .": "Единица измерения этого значения, мл, кг, шт. и т.д.", + "Dish, Service, Product, Modifier": "Блюдо, Услуга, Продукт, Модификатор", + "Weight is measured in grams 1000 = 1 kilogram": "Вес измеряется в граммах, 1000 г = 1 кг", + "Sorting order": "Порядок сортировки", + "Sorting is applied within a group": "Сортировка применяется внутри группы", + "Is Deleted": "Удален", + "Indicates whether the item is soft-deleted. If true, the item is not shown anywhere in the application and is considered removed.": "Указывает, удален ли элемент логически. Если значение true, элемент нигде не отображается в приложении и считается удаленным.", + "Parent Group": "Родительская группа", + "The group or category to which the dish belongs. This indicates the hierarchical relationship of the dish within its parent group or category.": "Группа или категория, к которой относится блюдо. Это указывает на иерархические отношения блюда в пределах его родительской группы или категории.", + "Tags": "Теги", + "A list of tags that can be applied to a dish or group. Tags help categorize and identify dishes based on attributes such as 'sweet', 'sour', 'salty', 'fried', 'boiled', 'hearty', 'fish', 'chicken', 'vegetarian', etc., making it easier to navigate the menu.": "Список тегов, которые можно применить к блюду или группе. Теги помогают классифицировать и идентифицировать блюда по таким атрибутам, как 'сладкий', 'кислый', 'соленый', 'жареный', 'вареный', 'сытный', 'рыба', 'курица', 'вегетарианский' и т.д., облегчая навигацию по меню.", + "Balance": "Остаток", + "Indicates the availability of the dish based on its balance. If the balance is 0, the dish is no longer available for sale. If the balance is -1, the dish is available for sale with no restrictions.": "Указывает на доступность блюда на основе его остатка. Если остаток равен 0, блюдо больше не доступно для продажи. Если остаток равен -1, блюдо доступно для продажи без ограничений.", + "Slug": "Slug", + "A URL-friendly version of the dish's name. It is used to create a readable and unique identifier for the dish in the URL, typically consisting of lowercase letters, numbers, and hyphens.": "Удобочитаемая версия названия блюда для URL. Используется для создания читаемого и уникального идентификатора блюда в URL, обычно состоящего из строчных букв, цифр и дефисов.", + "Concept": "Концепция", + "The concept represents a broad category or section that distinguishes dishes and menus. It can be used to define different sections such as 'Burgers', 'Pizza', 'Kids Menu', or even separate restaurants operating under the same backend but with different frontends. Concepts can also be used to manage delivery from different regions, making it a versatile and abstract categorization tool.": "Концепция представляет собой широкую категорию или раздел, который отличает блюда и меню. Может использоваться для определения различных разделов, таких как 'Бургеры', 'Пицца', 'Меню для детей' или даже отдельных ресторанов, работающих под одной серверной частью, но с разными фронтендами. Концепции также можно использовать для управления доставкой из различных регионов, что делает ее универсальным и абстрактным инструментом категоризации.", + "Visible": "Видимый", + "Indicates whether the dish is visible to users. If true, the dish will be displayed in the menu; if false, it will be hidden.": "Указывает, видно ли блюдо пользователям. Если true, блюдо будет отображаться в меню; если false, оно будет скрыто.", + "Modifier": "Модификатор", + "Indicates whether the dish is a modifier. A modifier is a dish that can alter or customize other dishes, often used to add options or variations.": "Указывает, является ли блюдо модификатором. Модификатор — это блюдо, которое может изменить или настроить другие блюда, часто используется для добавления опций или вариаций.", + "Promo": "Акция", + "Indicates whether the dish is part of a promotional offer. If true, the dish is featured as part of a special promotion or discount.": "Указывает, является ли блюдо частью промоакции. Если true, блюдо включено в специальное предложение или скидку.", + "Work Time": "Время работы", + "Specifies the operational hours during which the dish is available. This can include specific time ranges or days when the dish is offered.": "Указывает часы работы, в течение которых блюдо доступно. Это могут быть конкретные временные промежутки или дни, когда блюдо предлагается.", + "Modifiers": "Модификаторы", + "A list of modifiers applicable to the dish. Modifiers allow customization or variations of the dish, such as extra toppings or ingredient changes.": "Список модификаторов, применимых к блюду. Модификаторы позволяют настраивать или изменять блюдо, например, добавлять дополнительные ингредиенты или изменения в составе.", + "Images": "Изображения", + "A list of images associated with the dish. These images are used to visually represent the dish and can be selected for display in the menu.": "Список изображений, связанных с блюдом. Эти изображения используются для визуального представления блюда и могут быть выбраны для отображения в меню.", + "Recommendations": "Рекомендации", + "A list of dishes recommended based on this dish. It helps suggest similar or complementary items to users.": "Список блюд, рекомендованных на основе этого блюда. Помогает предложить пользователям похожие или дополнительные блюда.", + "Recommended By": "Рекомендовано", + "A self-referential link that connects this dish to other dishes as recommendations. It indicates that this dish recommends or is recommended by others.": "Само-ссылающаяся связь, которая соединяет это блюдо с другими блюдами в качестве рекомендаций. Указывает, что это блюдо рекомендует или рекомендуется другими.", + "Custom Data": "Пользовательские данные", + "Additional fields or data that can be set by modules or custom configurations. This allows for flexible extensions and metadata specific to the dish.": "Дополнительные поля или данные, которые могут быть установлены модулями или пользовательскими настройками. Это позволяет гибко расшир