mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
store product category create/list/update/delete
This commit is contained in:
parent
c3cdccfb74
commit
432b60ca9a
5
Gemfile
5
Gemfile
@ -50,9 +50,9 @@ group :test do
|
|||||||
gem 'faker'
|
gem 'faker'
|
||||||
gem 'minitest-reporters'
|
gem 'minitest-reporters'
|
||||||
gem 'pdf-reader'
|
gem 'pdf-reader'
|
||||||
|
gem 'rubyXL'
|
||||||
gem 'vcr', '6.0.0'
|
gem 'vcr', '6.0.0'
|
||||||
gem 'webmock'
|
gem 'webmock'
|
||||||
gem 'rubyXL'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production, :staging do
|
group :production, :staging do
|
||||||
@ -67,7 +67,6 @@ gem 'pg_search'
|
|||||||
# authentication
|
# authentication
|
||||||
gem 'devise', '>= 4.6.0'
|
gem 'devise', '>= 4.6.0'
|
||||||
|
|
||||||
|
|
||||||
gem 'omniauth', '~> 1.9.0'
|
gem 'omniauth', '~> 1.9.0'
|
||||||
gem 'omniauth-oauth2'
|
gem 'omniauth-oauth2'
|
||||||
gem 'omniauth_openid_connect'
|
gem 'omniauth_openid_connect'
|
||||||
@ -146,3 +145,5 @@ gem 'tzinfo-data'
|
|||||||
gem 'sassc', '= 2.1.0'
|
gem 'sassc', '= 2.1.0'
|
||||||
|
|
||||||
gem 'redis-session-store'
|
gem 'redis-session-store'
|
||||||
|
|
||||||
|
gem 'acts_as_list'
|
||||||
|
@ -48,6 +48,8 @@ GEM
|
|||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
|
acts_as_list (1.0.4)
|
||||||
|
activerecord (>= 4.2)
|
||||||
addressable (2.8.0)
|
addressable (2.8.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
@ -494,6 +496,7 @@ DEPENDENCIES
|
|||||||
aasm
|
aasm
|
||||||
actionpack-page_caching (= 1.2.2)
|
actionpack-page_caching (= 1.2.2)
|
||||||
active_record_query_trace
|
active_record_query_trace
|
||||||
|
acts_as_list
|
||||||
api-pagination
|
api-pagination
|
||||||
apipie-rails
|
apipie-rails
|
||||||
awesome_print
|
awesome_print
|
||||||
|
50
app/controllers/api/product_categories_controller.rb
Normal file
50
app/controllers/api/product_categories_controller.rb
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# API Controller for resources of type ProductCategory
|
||||||
|
# ProductCategorys are used to group products
|
||||||
|
class API::ProductCategoriesController < API::ApiController
|
||||||
|
before_action :authenticate_user!, except: :index
|
||||||
|
before_action :set_product_category, only: %i[show update destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@product_categories = ProductCategoryService.list
|
||||||
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize ProductCategory
|
||||||
|
@product_category = ProductCategory.new(product_category_params)
|
||||||
|
if @product_category.save
|
||||||
|
render status: :created
|
||||||
|
else
|
||||||
|
render json: @product_category.errors.full_messages, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @product_category
|
||||||
|
|
||||||
|
if @product_category.update(product_category_params)
|
||||||
|
render status: :ok
|
||||||
|
else
|
||||||
|
render json: @product_category.errors.full_messages, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @product_category
|
||||||
|
ProductCategoryService.destroy(@product_category)
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_product_category
|
||||||
|
@product_category = ProductCategory.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def product_category_params
|
||||||
|
params.require(:product_category).permit(:name, :parent_id, :slug, :position)
|
||||||
|
end
|
||||||
|
end
|
30
app/frontend/src/javascript/api/product-category.ts
Normal file
30
app/frontend/src/javascript/api/product-category.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import apiClient from './clients/api-client';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { ProductCategory } from '../models/product-category';
|
||||||
|
|
||||||
|
export default class ProductCategoryAPI {
|
||||||
|
static async index (): Promise<Array<ProductCategory>> {
|
||||||
|
const res: AxiosResponse<Array<ProductCategory>> = await apiClient.get('/api/product_categories');
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get (id: number): Promise<ProductCategory> {
|
||||||
|
const res: AxiosResponse<ProductCategory> = await apiClient.get(`/api/product_categories/${id}`);
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create (productCategory: ProductCategory): Promise<ProductCategory> {
|
||||||
|
const res: AxiosResponse<ProductCategory> = await apiClient.post('/api/product_categories', { product_category: productCategory });
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update (productCategory: ProductCategory): Promise<ProductCategory> {
|
||||||
|
const res: AxiosResponse<ProductCategory> = await apiClient.patch(`/api/product_categories/${productCategory.id}`, { product_category: productCategory });
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async destroy (productCategoryId: number): Promise<void> {
|
||||||
|
const res: AxiosResponse<void> = await apiClient.delete(`/api/product_categories/${productCategoryId}`);
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
}
|
@ -36,12 +36,10 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue,
|
|||||||
* If the default value changes, update the value of the input until there's no content in it.
|
* If the default value changes, update the value of the input until there's no content in it.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inputValue) {
|
|
||||||
setInputValue(defaultValue);
|
setInputValue(defaultValue);
|
||||||
if (typeof onChange === 'function') {
|
if (typeof onChange === 'function') {
|
||||||
onChange(defaultValue);
|
onChange(defaultValue);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [defaultValue]);
|
}, [defaultValue]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FabButton } from '../base/fab-button';
|
||||||
|
import { ProductCategory } from '../../models/product-category';
|
||||||
|
|
||||||
|
interface ProductCategoriesListProps {
|
||||||
|
productCategories: Array<ProductCategory>,
|
||||||
|
onEdit: (category: ProductCategory) => void,
|
||||||
|
onDelete: (categoryId: number) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component shows a Tree list of all Product's Categories
|
||||||
|
*/
|
||||||
|
export const ProductCategoriesList: React.FC<ProductCategoriesListProps> = ({ productCategories, onEdit, onDelete }) => {
|
||||||
|
/**
|
||||||
|
* Init the process of editing the given product category
|
||||||
|
*/
|
||||||
|
const editProductCategory = (category: ProductCategory): () => void => {
|
||||||
|
return (): void => {
|
||||||
|
onEdit(category);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the process of delete the given product category
|
||||||
|
*/
|
||||||
|
const deleteProductCategory = (categoryId: number): () => void => {
|
||||||
|
return (): void => {
|
||||||
|
onDelete(categoryId);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{productCategories.map((category) => (
|
||||||
|
<div key={category.id}>
|
||||||
|
{category.name}
|
||||||
|
<div className="buttons">
|
||||||
|
<FabButton className="edit-btn" onClick={editProductCategory(category)}>
|
||||||
|
<i className="fa fa-edit" />
|
||||||
|
</FabButton>
|
||||||
|
<FabButton className="delete-btn" onClick={deleteProductCategory(category.id)}>
|
||||||
|
<i className="fa fa-trash" />
|
||||||
|
</FabButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { react2angular } from 'react2angular';
|
||||||
|
import { HtmlTranslate } from '../base/html-translate';
|
||||||
|
import { Loader } from '../base/loader';
|
||||||
|
import { IApplication } from '../../models/application';
|
||||||
|
import { FabAlert } from '../base/fab-alert';
|
||||||
|
import { FabButton } from '../base/fab-button';
|
||||||
|
import { ProductCategoriesList } from './product-categories-list';
|
||||||
|
import { ProductCategoryModal } from './product-category-modal';
|
||||||
|
import { ProductCategory } from '../../models/product-category';
|
||||||
|
import ProductCategoryAPI from '../../api/product-category';
|
||||||
|
|
||||||
|
declare const Application: IApplication;
|
||||||
|
|
||||||
|
interface ProductCategoriesProps {
|
||||||
|
onSuccess: (message: string) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component shows a Tree list of all Product's Categories
|
||||||
|
*/
|
||||||
|
const ProductCategories: React.FC<ProductCategoriesProps> = ({ onSuccess, onError }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [isOpenProductCategoryModal, setIsOpenProductCategoryModal] = useState<boolean>(false);
|
||||||
|
const [productCategories, setProductCategories] = useState<Array<ProductCategory>>([]);
|
||||||
|
const [productCategory, setProductCategory] = useState<ProductCategory>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ProductCategoryAPI.index().then(data => {
|
||||||
|
setProductCategories(data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open create new product category modal
|
||||||
|
*/
|
||||||
|
const openProductCategoryModal = () => {
|
||||||
|
setIsOpenProductCategoryModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toggle create/edit product category modal
|
||||||
|
*/
|
||||||
|
const toggleCreateAndEditProductCategoryModal = () => {
|
||||||
|
setIsOpenProductCategoryModal(!isOpenProductCategoryModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* callback handle save product category success
|
||||||
|
*/
|
||||||
|
const onSaveProductCategorySuccess = (message: string) => {
|
||||||
|
setIsOpenProductCategoryModal(false);
|
||||||
|
onSuccess(message);
|
||||||
|
ProductCategoryAPI.index().then(data => {
|
||||||
|
setProductCategories(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open edit the product category modal
|
||||||
|
*/
|
||||||
|
const editProductCategory = (category: ProductCategory) => {
|
||||||
|
setProductCategory(category);
|
||||||
|
setIsOpenProductCategoryModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a product category
|
||||||
|
*/
|
||||||
|
const deleteProductCategory = async (categoryId: number): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await ProductCategoryAPI.destroy(categoryId);
|
||||||
|
const data = await ProductCategoryAPI.index();
|
||||||
|
setProductCategories(data);
|
||||||
|
onSuccess(t('app.admin.store.product_categories.successfully_deleted'));
|
||||||
|
} catch (e) {
|
||||||
|
onError(t('app.admin.store.product_categories.unable_to_delete') + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{t('app.admin.store.product_categories.the_categories')}</h2>
|
||||||
|
<FabButton className="save" onClick={openProductCategoryModal}>{t('app.admin.store.product_categories.create_a_product_category')}</FabButton>
|
||||||
|
<ProductCategoryModal isOpen={isOpenProductCategoryModal}
|
||||||
|
productCategories={productCategories}
|
||||||
|
productCategory={productCategory}
|
||||||
|
toggleModal={toggleCreateAndEditProductCategoryModal}
|
||||||
|
onSuccess={onSaveProductCategorySuccess}
|
||||||
|
onError={onError} />
|
||||||
|
<FabAlert level="warning">
|
||||||
|
<HtmlTranslate trKey="app.admin.store.product_categories.info" />
|
||||||
|
</FabAlert>
|
||||||
|
<ProductCategoriesList
|
||||||
|
productCategories={productCategories}
|
||||||
|
onEdit={editProductCategory}
|
||||||
|
onDelete={deleteProductCategory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProductCategoriesWrapper: React.FC<ProductCategoriesProps> = ({ onSuccess, onError }) => {
|
||||||
|
return (
|
||||||
|
<Loader>
|
||||||
|
<ProductCategories onSuccess={onSuccess} onError={onError} />
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Application.Components.component('productCategories', react2angular(ProductCategoriesWrapper, ['onSuccess', 'onError']));
|
@ -0,0 +1,99 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import slugify from 'slugify';
|
||||||
|
import { FabInput } from '../base/fab-input';
|
||||||
|
import { ProductCategory } from '../../models/product-category';
|
||||||
|
|
||||||
|
interface ProductCategoryFormProps {
|
||||||
|
productCategories: Array<ProductCategory>,
|
||||||
|
productCategory?: ProductCategory,
|
||||||
|
onChange: (field: string, value: string | number) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option format, expected by react-select
|
||||||
|
* @see https://github.com/JedWatson/react-select
|
||||||
|
*/
|
||||||
|
type selectOption = { value: number, label: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form to set create/edit supporting documents type
|
||||||
|
*/
|
||||||
|
export const ProductCategoryForm: React.FC<ProductCategoryFormProps> = ({ productCategories, productCategory, onChange }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
// filter all first level product categorie
|
||||||
|
const parents = productCategories.filter(c => !c.parent_id);
|
||||||
|
|
||||||
|
const [slug, setSlug] = useState<string>(productCategory?.slug || '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the default first level product category, formatted to match the react-select format
|
||||||
|
*/
|
||||||
|
const defaultValue = { value: productCategory?.parent_id, label: productCategory?.name };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert all parents to the react-select format
|
||||||
|
*/
|
||||||
|
const buildOptions = (): Array<selectOption> => {
|
||||||
|
return parents.map(t => {
|
||||||
|
return { value: t.id, label: t.name };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the selection of parent product category has changed.
|
||||||
|
*/
|
||||||
|
const handleCategoryParentChange = (option: selectOption): void => {
|
||||||
|
onChange('parent_id', option.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the name has changed.
|
||||||
|
*/
|
||||||
|
const handleNameChange = (value: string): void => {
|
||||||
|
onChange('name', value);
|
||||||
|
const _slug = slugify(value, { lower: true });
|
||||||
|
setSlug(_slug);
|
||||||
|
onChange('slug', _slug);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the slug has changed.
|
||||||
|
*/
|
||||||
|
const handleSlugChange = (value: string): void => {
|
||||||
|
onChange('slug', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="product-category-form">
|
||||||
|
<form name="productCategoryForm">
|
||||||
|
<div className="field">
|
||||||
|
<FabInput id="product_category_name"
|
||||||
|
icon={<i className="fa fa-edit" />}
|
||||||
|
defaultValue={productCategory?.name || ''}
|
||||||
|
placeholder={t('app.admin.store.product_category_form.name')}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
debounce={200}
|
||||||
|
required/>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<FabInput id="product_category_slug"
|
||||||
|
icon={<i className="fa fa-edit" />}
|
||||||
|
defaultValue={slug}
|
||||||
|
placeholder={t('app.admin.store.product_category_form.slug')}
|
||||||
|
onChange={handleSlugChange}
|
||||||
|
debounce={200}
|
||||||
|
required/>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<Select defaultValue={defaultValue}
|
||||||
|
placeholder={t('app.admin.store.product_category_form.select_parent_product_category')}
|
||||||
|
onChange={handleCategoryParentChange}
|
||||||
|
options={buildOptions()} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FabModal } from '../base/fab-modal';
|
||||||
|
import { ProductCategoryForm } from './product-category-form';
|
||||||
|
import { ProductCategory } from '../../models/product-category';
|
||||||
|
import ProductCategoryAPI from '../../api/product-category';
|
||||||
|
|
||||||
|
interface ProductCategoryModalProps {
|
||||||
|
isOpen: boolean,
|
||||||
|
toggleModal: () => void,
|
||||||
|
onSuccess: (message: string) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
productCategories: Array<ProductCategory>,
|
||||||
|
productCategory?: ProductCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if string is a valid url slug
|
||||||
|
*/
|
||||||
|
function checkIfValidURLSlug (str: string): boolean {
|
||||||
|
// Regular expression to check if string is a valid url slug
|
||||||
|
const regexExp = /^[a-z0-9]+(?:-[a-z0-9]+)*$/g;
|
||||||
|
|
||||||
|
return regexExp.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog to create/edit a category of product
|
||||||
|
*/
|
||||||
|
export const ProductCategoryModal: React.FC<ProductCategoryModalProps> = ({ isOpen, toggleModal, onSuccess, onError, productCategories, productCategory }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [data, setData] = useState<ProductCategory>({
|
||||||
|
id: productCategory?.id,
|
||||||
|
name: productCategory?.name || '',
|
||||||
|
slug: productCategory?.slug || '',
|
||||||
|
parent_id: productCategory?.parent_id,
|
||||||
|
position: productCategory?.position
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData({
|
||||||
|
id: productCategory?.id,
|
||||||
|
name: productCategory?.name || '',
|
||||||
|
slug: productCategory?.slug || '',
|
||||||
|
parent_id: productCategory?.parent_id,
|
||||||
|
position: productCategory?.position
|
||||||
|
});
|
||||||
|
}, [productCategory]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when an inner form field has changed: updates the internal state accordingly
|
||||||
|
*/
|
||||||
|
const handleChanged = (field: string, value: string | number) => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[field]: value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current product category to the API
|
||||||
|
*/
|
||||||
|
const handleSave = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (productCategory?.id) {
|
||||||
|
await ProductCategoryAPI.update(data);
|
||||||
|
onSuccess(t('app.admin.store.product_category_modal.successfully_updated'));
|
||||||
|
} else {
|
||||||
|
await ProductCategoryAPI.create(data);
|
||||||
|
onSuccess(t('app.admin.store.product_category_modal.successfully_created'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (productCategory?.id) {
|
||||||
|
onError(t('app.admin.store.product_category_modal.unable_to_update') + e);
|
||||||
|
} else {
|
||||||
|
onError(t('app.admin.store.product_category_modal.unable_to_create') + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the form is valid (not empty, url valid slug)
|
||||||
|
*/
|
||||||
|
const isPreventedSaveProductCategory = (): boolean => {
|
||||||
|
return !data.name || !data.slug || !checkIfValidURLSlug(data.slug);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FabModal title={t(`app.admin.store.product_category_modal.${productCategory ? 'edit' : 'new'}_product_category`)}
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggleModal={toggleModal}
|
||||||
|
closeButton={true}
|
||||||
|
confirmButton={t('app.admin.store.product_category_modal.save')}
|
||||||
|
onConfirm={handleSave}
|
||||||
|
preventConfirm={isPreventedSaveProductCategory()}>
|
||||||
|
<ProductCategoryForm productCategory={productCategory} productCategories={productCategories} onChange={handleChanged}/>
|
||||||
|
</FabModal>
|
||||||
|
);
|
||||||
|
};
|
38
app/frontend/src/javascript/controllers/admin/store.js
Normal file
38
app/frontend/src/javascript/controllers/admin/store.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/* eslint-disable
|
||||||
|
no-return-assign,
|
||||||
|
no-undef,
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
Application.Controllers.controller('AdminStoreController', ['$scope', 'CSRF', 'growl',
|
||||||
|
function ($scope, CSRF, growl) {
|
||||||
|
/* PUBLIC SCOPE */
|
||||||
|
/**
|
||||||
|
* Callback triggered in case of error
|
||||||
|
*/
|
||||||
|
$scope.onError = (message) => {
|
||||||
|
growl.error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered in case of success
|
||||||
|
*/
|
||||||
|
$scope.onSuccess = (message) => {
|
||||||
|
growl.success(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* PRIVATE SCOPE */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
*/
|
||||||
|
const initialize = function () {
|
||||||
|
// set the authenticity tokens in the forms
|
||||||
|
CSRF.setMetaTags();
|
||||||
|
};
|
||||||
|
|
||||||
|
// init the controller (call at the end !)
|
||||||
|
return initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
]);
|
@ -83,6 +83,12 @@ Application.Controllers.controller('MainNavController', ['$scope', 'settingsProm
|
|||||||
linkIcon: 'cogs',
|
linkIcon: 'cogs',
|
||||||
authorizedRoles: ['admin', 'manager']
|
authorizedRoles: ['admin', 'manager']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
state: 'app.admin.store',
|
||||||
|
linkText: 'app.public.common.manage_the_store',
|
||||||
|
linkIcon: 'cogs',
|
||||||
|
authorizedRoles: ['admin', 'manager']
|
||||||
|
},
|
||||||
$scope.$root.modules.trainings && {
|
$scope.$root.modules.trainings && {
|
||||||
state: 'app.admin.trainings',
|
state: 'app.admin.trainings',
|
||||||
linkText: 'app.public.common.trainings_monitoring',
|
linkText: 'app.public.common.trainings_monitoring',
|
||||||
|
7
app/frontend/src/javascript/models/product-category.ts
Normal file
7
app/frontend/src/javascript/models/product-category.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface ProductCategory {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
slug: string,
|
||||||
|
parent_id?: number,
|
||||||
|
position: number,
|
||||||
|
}
|
@ -1104,6 +1104,16 @@ angular.module('application.router', ['ui.router'])
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.state('app.admin.store', {
|
||||||
|
url: '/admin/store',
|
||||||
|
views: {
|
||||||
|
'main@': {
|
||||||
|
templateUrl: '/admin/store/index.html',
|
||||||
|
controller: 'AdminStoreController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// OpenAPI Clients
|
// OpenAPI Clients
|
||||||
.state('app.admin.open_api_clients', {
|
.state('app.admin.open_api_clients', {
|
||||||
url: '/open_api_clients',
|
url: '/open_api_clients',
|
||||||
|
1
app/frontend/templates/admin/store/categories.html
Normal file
1
app/frontend/templates/admin/store/categories.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<product-categories on-success="onSuccess" on-error="onError"/>
|
42
app/frontend/templates/admin/store/index.html
Normal file
42
app/frontend/templates/admin/store/index.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<section class="heading b-b">
|
||||||
|
<div class="row no-gutter">
|
||||||
|
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||||
|
<section class="heading-btn">
|
||||||
|
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||||
|
<section class="heading-title">
|
||||||
|
<h1 translate>{{ 'app.admin.store.manage_the_store' }}</h1>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="m-lg admin-store-manage">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<uib-tabset justified="true" active="tabs.active">
|
||||||
|
|
||||||
|
<uib-tab heading="{{ 'app.admin.store.settings' | translate }}" index="0">
|
||||||
|
<ng-include src="'/admin/store/settings.html'"></ng-include>
|
||||||
|
</uib-tab>
|
||||||
|
|
||||||
|
<uib-tab heading="{{ 'app.admin.store.all_products' | translate }}" index="1">
|
||||||
|
<ng-include src="'/admin/store/products.html'"></ng-include>
|
||||||
|
</uib-tab>
|
||||||
|
|
||||||
|
<uib-tab heading="{{ 'app.admin.store.categories_of_store' | translate }}" index="2">
|
||||||
|
<ng-include src="'/admin/store/categories.html'"></ng-include>
|
||||||
|
</uib-tab>
|
||||||
|
|
||||||
|
<uib-tab heading="{{ 'app.admin.store.the_orders' | translate }}" index="3">
|
||||||
|
<ng-include src="'/admin/store/orders.html'"></ng-include>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
1
app/frontend/templates/admin/store/orders.html
Normal file
1
app/frontend/templates/admin/store/orders.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h2>Orders page</h2>
|
1
app/frontend/templates/admin/store/products.html
Normal file
1
app/frontend/templates/admin/store/products.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h2>Products page</h2>
|
1
app/frontend/templates/admin/store/settings.html
Normal file
1
app/frontend/templates/admin/store/settings.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h2>Settings page</h2>
|
12
app/models/product_category.rb
Normal file
12
app/models/product_category.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Category is a first-level filter, used to categorize Products.
|
||||||
|
# It is mandatory to choose a Category when creating a Product.
|
||||||
|
class ProductCategory < ApplicationRecord
|
||||||
|
validates :name, :slug, presence: true
|
||||||
|
|
||||||
|
belongs_to :parent, class_name: 'ProductCategory'
|
||||||
|
has_many :children, class_name: 'ProductCategory', foreign_key: :parent_id
|
||||||
|
|
||||||
|
acts_as_list scope: :parent
|
||||||
|
end
|
16
app/policies/product_category_policy.rb
Normal file
16
app/policies/product_category_policy.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Check the access policies for API::ProductCategoriesController
|
||||||
|
class ProductCategoryPolicy < ApplicationPolicy
|
||||||
|
def create?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
end
|
13
app/services/product_category_service.rb
Normal file
13
app/services/product_category_service.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods for ProductCategory
|
||||||
|
class ProductCategoryService
|
||||||
|
def self.list
|
||||||
|
ProductCategory.all.order(parent_id: :asc, position: :asc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.destroy(product_category)
|
||||||
|
ProductCategory.where(parent_id: product_category.id).destroy_all
|
||||||
|
product_category.destroy
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.extract! product_category, :id, :name, :slug, :parent_id, :position
|
3
app/views/api/product_categories/create.json.jbuilder
Normal file
3
app/views/api/product_categories/create.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.partial! 'api/product_categories/product_category', product_category: @product_category
|
5
app/views/api/product_categories/index.json.jbuilder
Normal file
5
app/views/api/product_categories/index.json.jbuilder
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.array! @product_categories do |product_category|
|
||||||
|
json.partial! 'api/product_categories/product_category', product_category: product_category
|
||||||
|
end
|
3
app/views/api/product_categories/show.json.jbuilder
Normal file
3
app/views/api/product_categories/show.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.partial! 'api/product_categories/product_category', product_category: @product_category
|
3
app/views/api/product_categories/update.json.jbuilder
Normal file
3
app/views/api/product_categories/update.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.partial! 'api/product_categories/product_category', product_category: @product_category
|
@ -1893,3 +1893,27 @@ en:
|
|||||||
doc:
|
doc:
|
||||||
title: "Documentation"
|
title: "Documentation"
|
||||||
content: "Click here to access the API online documentation."
|
content: "Click here to access the API online documentation."
|
||||||
|
store:
|
||||||
|
manage_the_store: "Manage the Store Fablab"
|
||||||
|
settings: "Settings"
|
||||||
|
all_products: "All products"
|
||||||
|
categories_of_store: "Categories of store"
|
||||||
|
the_orders: "Orders"
|
||||||
|
product_categories:
|
||||||
|
create_a_product_category: "Create a category"
|
||||||
|
the_categories: "Categories"
|
||||||
|
info: "<strong>Information:</strong></br>Find below all the categories created. The categories are arranged on two levels maximum, you can arrange them with a drag and drop. The order of the categories will be identical on the public view and the list below. Please note that you can delete a category or a sub-category even if they are associated with products. The latter will be left without categories. If you delete a category that contains sub-categories, the latter will also be deleted. <strong>Make sure that your categories are well arranged and save your choice.</strong>"
|
||||||
|
successfully_deleted: "The category has been successfully deleted"
|
||||||
|
unable_to_delete: "Unable to delete the category: "
|
||||||
|
product_category_modal:
|
||||||
|
successfully_created: "The new category has been created."
|
||||||
|
unable_to_create: "Unable to create the category: "
|
||||||
|
successfully_updated: "The category has been updated."
|
||||||
|
unable_to_update: "Unable to modify the category: "
|
||||||
|
new_product_category: "Create a category"
|
||||||
|
edit_product_category: "Modify a category"
|
||||||
|
save: "Save"
|
||||||
|
product_category_form:
|
||||||
|
name: "Name of category"
|
||||||
|
slug: "Name of URL"
|
||||||
|
select_parent_product_category: "Choose a parent category (N1)"
|
||||||
|
@ -1893,3 +1893,27 @@ fr:
|
|||||||
doc:
|
doc:
|
||||||
title: "Documentation"
|
title: "Documentation"
|
||||||
content: "Cliquez ici pour accéder à la documentation en ligne de l'API."
|
content: "Cliquez ici pour accéder à la documentation en ligne de l'API."
|
||||||
|
store:
|
||||||
|
manage_the_store: "Gestion de la Boutique Fablab"
|
||||||
|
settings: "Paramètres"
|
||||||
|
all_products: "Tous les produits"
|
||||||
|
categories_of_store: "Les catégories de la boutique"
|
||||||
|
the_orders: "Les commandes"
|
||||||
|
product_categories:
|
||||||
|
create_a_product_category: "Créer une catégorie"
|
||||||
|
the_categories: "Les catégories"
|
||||||
|
info: "<strong>Information:</strong></br>Retrouvez ci-dessous toutes les catégories créés. Les catégories se rangent sur deux niveux maximum, vous pouvez les agancer avec un glisser-déposer. L'ordre d'affichage des catégories sera identique sur la vue publique et la liste ci-dessous. Attention, Vous pouvez supprimer une catégorie ou une sous-catégorie même si elles sont associées à des produits. Ces derniers se retrouveront sans catégories. Si vous supprimez une catégorie contenant des sous-catégories, ces dernières seront elles aussi supprimées. <strong>Veillez au bon agencement de vos catégories et sauvegarder votre choix.</strong>"
|
||||||
|
successfully_deleted: "La catégorie a bien été supprimé"
|
||||||
|
unable_to_delete: "Impossible de supprimer the category: "
|
||||||
|
product_category_modal:
|
||||||
|
successfully_created: "La catégorie a bien été créée."
|
||||||
|
unable_to_create: "Impossible de créer la catégorie : "
|
||||||
|
successfully_updated: "La nouvelle catégorie a bien été mise à jour."
|
||||||
|
unable_to_update: "Impossible de modifier la catégorie : "
|
||||||
|
new_product_category: "Créer une catégorie"
|
||||||
|
edit_product_category: "Modifier la catéogirie"
|
||||||
|
save: "Sauvgarder"
|
||||||
|
product_category_form:
|
||||||
|
name: "Nom de la catégorie"
|
||||||
|
slug: "Nom de l'URL"
|
||||||
|
select_parent_product_category: "Choisir une catégorie parent (N1)"
|
||||||
|
@ -51,6 +51,7 @@ en:
|
|||||||
subscriptions_and_prices: "Subscriptions and Prices"
|
subscriptions_and_prices: "Subscriptions and Prices"
|
||||||
manage_the_events: "Events"
|
manage_the_events: "Events"
|
||||||
manage_the_machines: "Machines"
|
manage_the_machines: "Machines"
|
||||||
|
manage_the_store: "Store"
|
||||||
manage_the_spaces: "Spaces"
|
manage_the_spaces: "Spaces"
|
||||||
projects: "Projects"
|
projects: "Projects"
|
||||||
statistics: "Statistics"
|
statistics: "Statistics"
|
||||||
|
@ -51,6 +51,7 @@ fr:
|
|||||||
subscriptions_and_prices: "Abonnements & Tarifs"
|
subscriptions_and_prices: "Abonnements & Tarifs"
|
||||||
manage_the_events: "Événements"
|
manage_the_events: "Événements"
|
||||||
manage_the_machines: "Machines"
|
manage_the_machines: "Machines"
|
||||||
|
manage_the_store: "Gestion de la boutique"
|
||||||
manage_the_spaces: "Espaces"
|
manage_the_spaces: "Espaces"
|
||||||
projects: "Projets"
|
projects: "Projets"
|
||||||
statistics: "Statistiques"
|
statistics: "Statistiques"
|
||||||
|
@ -150,6 +150,8 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :profile_custom_fields
|
resources :profile_custom_fields
|
||||||
|
|
||||||
|
resources :product_categories
|
||||||
|
|
||||||
# for admin
|
# for admin
|
||||||
resources :trainings do
|
resources :trainings do
|
||||||
get :availabilities, on: :member
|
get :availabilities, on: :member
|
||||||
|
12
db/migrate/20220620072750_create_product_categories.rb
Normal file
12
db/migrate/20220620072750_create_product_categories.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class CreateProductCategories < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :product_categories do |t|
|
||||||
|
t.string :name
|
||||||
|
t.string :slug
|
||||||
|
t.integer :parent_id, index: true
|
||||||
|
t.integer :position
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
db/schema.rb
30
db/schema.rb
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
enable_extension "unaccent"
|
enable_extension "unaccent"
|
||||||
|
|
||||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||||
t.integer "signaled_id"
|
|
||||||
t.string "signaled_type"
|
t.string "signaled_type"
|
||||||
|
t.integer "signaled_id"
|
||||||
t.string "first_name"
|
t.string "first_name"
|
||||||
t.string "last_name"
|
t.string "last_name"
|
||||||
t.string "email"
|
t.string "email"
|
||||||
@ -49,8 +49,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
t.string "locality"
|
t.string "locality"
|
||||||
t.string "country"
|
t.string "country"
|
||||||
t.string "postal_code"
|
t.string "postal_code"
|
||||||
t.integer "placeable_id"
|
|
||||||
t.string "placeable_type"
|
t.string "placeable_type"
|
||||||
|
t.integer "placeable_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
end
|
end
|
||||||
@ -64,8 +64,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "assets", id: :serial, force: :cascade do |t|
|
create_table "assets", id: :serial, force: :cascade do |t|
|
||||||
t.integer "viewable_id"
|
|
||||||
t.string "viewable_type"
|
t.string "viewable_type"
|
||||||
|
t.integer "viewable_id"
|
||||||
t.string "attachment"
|
t.string "attachment"
|
||||||
t.string "type"
|
t.string "type"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -146,8 +146,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "credits", id: :serial, force: :cascade do |t|
|
create_table "credits", id: :serial, force: :cascade do |t|
|
||||||
t.integer "creditable_id"
|
|
||||||
t.string "creditable_type"
|
t.string "creditable_type"
|
||||||
|
t.integer "creditable_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "hours"
|
t.integer "hours"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -369,15 +369,15 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
|
|
||||||
create_table "notifications", id: :serial, force: :cascade do |t|
|
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||||
t.integer "receiver_id"
|
t.integer "receiver_id"
|
||||||
t.integer "attached_object_id"
|
|
||||||
t.string "attached_object_type"
|
t.string "attached_object_type"
|
||||||
|
t.integer "attached_object_id"
|
||||||
t.integer "notification_type_id"
|
t.integer "notification_type_id"
|
||||||
t.boolean "is_read", default: false
|
t.boolean "is_read", default: false
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.string "receiver_type"
|
t.string "receiver_type"
|
||||||
t.boolean "is_send", default: false
|
t.boolean "is_send", default: false
|
||||||
t.jsonb "meta_data", default: {}
|
t.jsonb "meta_data", default: "{}"
|
||||||
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||||
end
|
end
|
||||||
@ -570,8 +570,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
create_table "prices", id: :serial, force: :cascade do |t|
|
create_table "prices", id: :serial, force: :cascade do |t|
|
||||||
t.integer "group_id"
|
t.integer "group_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "priceable_id"
|
|
||||||
t.string "priceable_type"
|
t.string "priceable_type"
|
||||||
|
t.integer "priceable_id"
|
||||||
t.integer "amount"
|
t.integer "amount"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
@ -581,6 +581,16 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
t.index ["priceable_type", "priceable_id"], name: "index_prices_on_priceable_type_and_priceable_id"
|
t.index ["priceable_type", "priceable_id"], name: "index_prices_on_priceable_type_and_priceable_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "product_categories", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.string "slug"
|
||||||
|
t.integer "parent_id"
|
||||||
|
t.integer "position"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["parent_id"], name: "index_product_categories_on_parent_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "profile_custom_fields", force: :cascade do |t|
|
create_table "profile_custom_fields", force: :cascade do |t|
|
||||||
t.string "label"
|
t.string "label"
|
||||||
t.boolean "required", default: false
|
t.boolean "required", default: false
|
||||||
@ -729,8 +739,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
t.text "message"
|
t.text "message"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.integer "reservable_id"
|
|
||||||
t.string "reservable_type"
|
t.string "reservable_type"
|
||||||
|
t.integer "reservable_id"
|
||||||
t.integer "nb_reserve_places"
|
t.integer "nb_reserve_places"
|
||||||
t.integer "statistic_profile_id"
|
t.integer "statistic_profile_id"
|
||||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
||||||
@ -739,8 +749,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
|
|
||||||
create_table "roles", id: :serial, force: :cascade do |t|
|
create_table "roles", id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.integer "resource_id"
|
|
||||||
t.string "resource_type"
|
t.string "resource_type"
|
||||||
|
t.integer "resource_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
||||||
@ -1020,8 +1030,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
|
|||||||
t.boolean "is_allow_newsletter"
|
t.boolean "is_allow_newsletter"
|
||||||
t.inet "current_sign_in_ip"
|
t.inet "current_sign_in_ip"
|
||||||
t.inet "last_sign_in_ip"
|
t.inet "last_sign_in_ip"
|
||||||
t.string "mapped_from_sso"
|
|
||||||
t.datetime "validated_at"
|
t.datetime "validated_at"
|
||||||
|
t.string "mapped_from_sso"
|
||||||
t.index ["auth_token"], name: "index_users_on_auth_token"
|
t.index ["auth_token"], name: "index_users_on_auth_token"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
|
@ -139,12 +139,15 @@
|
|||||||
"react-i18next": "^11.15.6",
|
"react-i18next": "^11.15.6",
|
||||||
"react-modal": "^3.11.2",
|
"react-modal": "^3.11.2",
|
||||||
"react-select": "^5.3.2",
|
"react-select": "^5.3.2",
|
||||||
|
"react-sortablejs": "^6.1.4",
|
||||||
"react-switch": "^6.0.0",
|
"react-switch": "^6.0.0",
|
||||||
"react2angular": "^4.0.6",
|
"react2angular": "^4.0.6",
|
||||||
"resolve-url-loader": "^4.0.0",
|
"resolve-url-loader": "^4.0.0",
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.49.9",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"shakapacker": "6.2.0",
|
"shakapacker": "6.2.0",
|
||||||
|
"slugify": "^1.6.5",
|
||||||
|
"sortablejs": "^1.15.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"summernote": "0.8.18",
|
"summernote": "0.8.18",
|
||||||
"terser-webpack-plugin": "5",
|
"terser-webpack-plugin": "5",
|
||||||
|
28
yarn.lock
28
yarn.lock
@ -3317,6 +3317,11 @@ chrome-trace-event@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
|
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
|
||||||
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
|
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
|
||||||
|
|
||||||
|
classnames@2.3.1:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||||
|
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
|
||||||
clean-css@^4.2.3:
|
clean-css@^4.2.3:
|
||||||
version "4.2.4"
|
version "4.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
|
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
|
||||||
@ -6600,6 +6605,14 @@ react-select@^5.3.2:
|
|||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
react-transition-group "^4.3.0"
|
react-transition-group "^4.3.0"
|
||||||
|
|
||||||
|
react-sortablejs@^6.1.4:
|
||||||
|
version "6.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-sortablejs/-/react-sortablejs-6.1.4.tgz#420ebfab602bbd935035dec24a04c8b3b836dbbf"
|
||||||
|
integrity sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==
|
||||||
|
dependencies:
|
||||||
|
classnames "2.3.1"
|
||||||
|
tiny-invariant "1.2.0"
|
||||||
|
|
||||||
react-switch@^6.0.0:
|
react-switch@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-switch/-/react-switch-6.0.0.tgz#bd4a2dea08f211b8a32e55e8314fd44bc1ec947e"
|
resolved "https://registry.yarnpkg.com/react-switch/-/react-switch-6.0.0.tgz#bd4a2dea08f211b8a32e55e8314fd44bc1ec947e"
|
||||||
@ -7098,6 +7111,11 @@ slash@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||||
|
|
||||||
|
slugify@^1.6.5:
|
||||||
|
version "1.6.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8"
|
||||||
|
integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==
|
||||||
|
|
||||||
sockjs@^0.3.21:
|
sockjs@^0.3.21:
|
||||||
version "0.3.21"
|
version "0.3.21"
|
||||||
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417"
|
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417"
|
||||||
@ -7107,6 +7125,11 @@ sockjs@^0.3.21:
|
|||||||
uuid "^3.4.0"
|
uuid "^3.4.0"
|
||||||
websocket-driver "^0.7.4"
|
websocket-driver "^0.7.4"
|
||||||
|
|
||||||
|
sortablejs@^1.15.0:
|
||||||
|
version "1.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a"
|
||||||
|
integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==
|
||||||
|
|
||||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
|
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
@ -7380,6 +7403,11 @@ timsort@^0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||||
|
|
||||||
|
tiny-invariant@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
||||||
|
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
|
||||||
|
|
||||||
tippy.js@^6.3.7:
|
tippy.js@^6.3.7:
|
||||||
version "6.3.7"
|
version "6.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
|
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user