├── src ├── declarations.d.ts ├── pages │ ├── TableListing.vue │ ├── FormPage.vue │ ├── TableListingDetail.vue │ ├── DashboardPage.vue │ ├── AlertsPage.vue │ └── ComponentsPage.vue ├── assets │ └── logo.png ├── auto-imports.d.ts ├── types │ ├── index.ts │ └── Product.ts ├── mocks │ ├── top-categories.json │ ├── top-products.json │ ├── products.ts │ ├── roles.json │ └── products.json ├── components │ ├── ProductsListing │ │ ├── ProductDetail.vue │ │ ├── ProductsListing.unit.ts │ │ ├── ProductsPage.vue │ │ ├── PrdocutsListingBody │ │ │ ├── ProductsListingBody.css │ │ │ └── ProductsListingBody.vue │ │ ├── ProductsListing.css │ │ ├── ProductsListingHeader │ │ │ ├── ProductsListingHeader.vue │ │ │ └── ProductsListingFiltersModal.vue │ │ └── ProductsListing.vue │ ├── LoadingOverlay │ │ └── LoadingOverlay.vue │ ├── Breadcrumbs │ │ ├── LinkBreadcrumb.vue │ │ └── Breadcrumbs.vue │ ├── InputLabel │ │ └── InputLabel.vue │ ├── Statistics │ │ └── Statistics.vue │ ├── ColumnTemplates │ │ ├── ProductStockColumnTemplate.vue │ │ ├── StringColumnTemplate.vue │ │ ├── ImageColumnTemplate.vue │ │ ├── ProductActionsColumnTemplate.vue │ │ └── StatusColumnTemplate.vue │ ├── TopStatistics │ │ └── TopStatistics.vue │ ├── ImagePlaceholderWrapper │ │ └── ImagePlaceholderWrapper.vue │ └── UserForm │ │ └── UserForm.vue ├── composables │ ├── useAddonProperties.ts │ ├── useDeleteProductMutation.ts │ ├── useProductListingFilters.ts │ ├── useFetchTopProductsQuery.ts │ ├── useFetchTopCategoriesQuery.ts │ ├── useDeleteProductsMutation.ts │ └── useFetchProductsQuery.ts ├── standalone.ts ├── router.ts ├── vite-env.d.ts ├── app.css ├── add-on.ts ├── App.vue ├── vue-bootstrap.ts ├── utils.ts ├── index.html ├── manifest.ts ├── components.d.ts └── routes.ts ├── server-ssl ├── default.conf ├── README.md └── generate-ssl.sh ├── .lintstagedrc.cjs ├── postcss.config.cjs ├── .env.example ├── scripts └── prepare.sh ├── .editorconfig ├── tailwind.config.cjs ├── tsconfig.node.json ├── .gitignore ├── vitest-setup.ts ├── LICENSE ├── tsconfig.json ├── public └── vite.svg ├── renovate.json5 ├── .eslintrc.cjs ├── vite-plugins ├── aboutYouAddonLoader.ts ├── aboutYouGlobalWindowEventLoader.ts ├── aboutYouVueStyleLoader.ts └── aboutYouStyleLoader.ts ├── README.md ├── package.json └── vite.config.ts /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module 'dom-storage'; 3 | -------------------------------------------------------------------------------- /src/pages/TableListing.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scayle/demo-add-on-vite/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /server-ssl/default.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | distinguished_name=req 3 | [SAN] 4 | subjectAltName=DNS:{{SERVER_HOST}} 5 | -------------------------------------------------------------------------------- /src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{ts,js,cjs,vue}': [ 3 | 'bash -c "vue-tsc --noEmit"', 4 | 'eslint --fix' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { RouteDefinition } from "@scayle/add-on-utils"; 2 | 3 | export type GroupRouteDefinition = RouteDefinition | { isGroup: boolean } 4 | -------------------------------------------------------------------------------- /server-ssl/README.md: -------------------------------------------------------------------------------- 1 | The vite dev server looks in this folder for a default.crt and default.key file to use for https. 2 | 3 | **To generate ssl certificate run** `npm run generate:ssl` 4 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | 'postcss-logical': {}, 6 | 'postcss-dir-pseudo-class': { preserve: true } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/mocks/top-categories.json: -------------------------------------------------------------------------------- 1 | [{"id":1,"name":"Clothes","sold":120}, 2 | {"id":2,"name":"Shoes","sold":88}, 3 | {"id":3,"name":"Glasses","sold":55}, 4 | {"id":4,"name":"Hats","sold":32}, 5 | {"id":5,"name":"Watches","sold":15}] 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ENV not exposed to application 2 | CONFIG_SERVER_HOST=demo-add-on.cloud-panel.aboutyou.test 3 | CONFIG_SERVER_PORT=8082 4 | 5 | # ENV exposed application (with PANEL_, prefix) 6 | PANEL_ADDON_IDENTIFIER=demo 7 | PANEL_USE_SHADOW_DOM=true 8 | -------------------------------------------------------------------------------- /scripts/prepare.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | ROOT_DIR=$SCRIPT_DIR/../ 5 | 6 | cd "$ROOT_DIR" 7 | 8 | if ! [ -f ".env" ]; then 9 | cp .env.example .env 10 | fi 11 | -------------------------------------------------------------------------------- /src/components/ProductsListing/ProductDetail.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/mocks/top-products.json: -------------------------------------------------------------------------------- 1 | [{"id":1,"name":"\"Tim\" Pullover","sold":55}, 2 | {"id":2,"name":"\"Dan\" Sneaker","sold":50}, 3 | {"id":3,"name":"\"Hermine\" Schal","sold":26}, 4 | {"id":4,"name":"\"Susi\" Shirt","sold":18}, 5 | {"id":5,"name":"\"Dana\" Sportshirt","sold":5}] 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.yml] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/types/Product.ts: -------------------------------------------------------------------------------- 1 | export type ProductStatus = 'blocked' | 'problem' | 'live'; 2 | 3 | export type Product = { 4 | id: number; 5 | product_id: number; 6 | name: string, 7 | merchant: string; 8 | status: ProductStatus; 9 | image_hash: string; 10 | stock: { stockCount: string; variantCount: number; } 11 | }; 12 | -------------------------------------------------------------------------------- /src/composables/useAddonProperties.ts: -------------------------------------------------------------------------------- 1 | import { inject, InjectionKey } from 'vue'; 2 | import { AddOnCustomProps } from '@scayle/add-on-utils'; 3 | 4 | export const ADDON_PROPERTIES_KEY: InjectionKey = Symbol('addonPropertiesKey'); 5 | 6 | export default function useAddonProperties() { 7 | return inject(ADDON_PROPERTIES_KEY); 8 | } 9 | -------------------------------------------------------------------------------- /src/standalone.ts: -------------------------------------------------------------------------------- 1 | // This entrypoint is used to create a standalone version of the app as 2 | // an alternative to loading it as an add-on 3 | import { createApp } from 'vue'; 4 | import { initVuePlugins } from '@/vue-bootstrap'; 5 | import App from './App.vue'; 6 | 7 | const app = createApp(App); 8 | 9 | initVuePlugins(app); 10 | 11 | app.mount("#app"); 12 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; 2 | import { routes } from '@/routes'; 3 | import { BASE_URL } from './utils'; 4 | 5 | export default createRouter({ 6 | routes: routes as RouteRecordRaw[], 7 | // Make sure to set the base because all add-on pages will be under add-ons/demo 8 | history: createWebHashHistory(BASE_URL), 9 | }); 10 | 11 | -------------------------------------------------------------------------------- /src/components/LoadingOverlay/LoadingOverlay.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require('@scayle/tailwind-base')], 3 | mode: 'jit', 4 | content: [ 5 | 'src/**/*.js', 6 | 'src/**/*.ts', 7 | 'src/**/*.vue', 8 | 'src/**/*.html', 9 | ], 10 | plugins: [ 11 | require('tailwindcss-logical'), 12 | require('@scayle/tailwind-base/plugins/base.js'), 13 | require('@scayle/tailwind-base/plugins/element-plus.js') 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /src/composables/useDeleteProductMutation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { Product } from '@/types/Product'; 5 | import useDeleteProductsMutation from '@/composables/useDeleteProductsMutation'; 6 | 7 | export default function useDeleteProductMutation() { 8 | const { deleteProducts, ...rest } = useDeleteProductsMutation(); 9 | const deleteProduct = (id: Product['id']) => deleteProducts([id]); 10 | 11 | return { 12 | ...rest, 13 | deleteProduct 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/composables/useProductListingFilters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | 5 | import { ref } from 'vue'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import { ProductsFilters } from '@/composables/useFetchProductsQuery'; 11 | 12 | export default function useProductListingFilters() { 13 | const page = ref(1); 14 | const perPage = ref(30); 15 | const filters = ref({}); 16 | 17 | return { 18 | page, 19 | perPage, 20 | filters, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ProductsListing/ProductsListing.unit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { mount } from '@vue/test-utils'; 5 | import { it, describe, expect } from 'vitest'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import ProductsListing from './ProductsListing.vue'; 11 | 12 | describe('ProductsListing.vue', () => { 13 | it('can be render', () => { 14 | const wrapper = mount(ProductsListing); 15 | 16 | expect(wrapper.find('.spinner').exists()).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | import type { AddOnCustomProps } from '@scayle/add-on-utils'; 2 | 3 | /// 4 | 5 | declare module '*.vue' { 6 | import type { DefineComponent } from 'vue' 7 | const component: DefineComponent<{}, {}, any> 8 | export default component 9 | } 10 | 11 | // TODO: Could this be added to the add-on-utils and added automatically? 12 | // How does vue-router do it? 13 | declare module '@vue/runtime-core' { 14 | interface ComponentCustomProperties { 15 | $addOn: AddOnCustomProps 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": [ 8 | "./src/*" 9 | ] 10 | }, 11 | "moduleResolution": "Node", 12 | "lib": [ 13 | "ESNext" 14 | ], 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": [ 18 | "vite.config.ts", 19 | "vite.add-on.config.ts", 20 | "vitest-setup.ts", 21 | "vite-plugins/**/*.ts", 22 | "src/declarations.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | server-ssl/* 27 | !server-ssl/README.md 28 | !server-ssl/generate-ssl.sh 29 | !server-ssl/default.conf 30 | 31 | .env 32 | coverage 33 | junit.xml 34 | 35 | # ignore types generated from unit tests (src are the vital ones) 36 | auto-imports.d.ts 37 | components.d.ts 38 | -------------------------------------------------------------------------------- /src/composables/useFetchTopProductsQuery.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { useQuery } from '@tanstack/vue-query'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { startTimeout } from '@/utils'; 10 | 11 | export default function useFetchTopProductsQuery() { 12 | const { 13 | data, 14 | isLoading, 15 | } = useQuery(['top-products'], async () => { 16 | await startTimeout(1000); 17 | const { default: products } = await import('@/mocks/top-products.json'); 18 | 19 | return products; 20 | }); 21 | 22 | return { 23 | isLoading, 24 | products: data, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/composables/useFetchTopCategoriesQuery.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { useQuery } from '@tanstack/vue-query'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { startTimeout } from '@/utils'; 10 | 11 | export default function useFetchTopCategoriesQuery() { 12 | const { 13 | data, 14 | isLoading, 15 | } = useQuery(['top-categories'], async () => { 16 | await startTimeout(2000); 17 | const { default: categories } = await import('@/mocks/top-categories.json'); 18 | 19 | return categories; 20 | }); 21 | 22 | return { 23 | isLoading, 24 | categories: data, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .single-spa-container { 6 | height: 100%; 7 | overflow: hidden; 8 | } 9 | 10 | .ay-input, 11 | .ay-input .ay-input__inner, 12 | .ay-input input { 13 | width: 100%; 14 | } 15 | 16 | .ay-select, 17 | .ay-select .ay-popper__reference { 18 | width: 100%; 19 | } 20 | 21 | /** 22 | Temp fix until we use more than one selector 23 | to style dashboard statistics icon 24 | as it gets overriden by .icon 25 | */ 26 | .ay-dashboard-statistic .ay-dashboard-statistic__icon { 27 | display: inline-flex; 28 | height: 60px; 29 | width: 60px; 30 | } 31 | 32 | .single-spa-container { 33 | --el-color-primary: #0dcc8d !important; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs/LinkBreadcrumb.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 37 | -------------------------------------------------------------------------------- /src/add-on.ts: -------------------------------------------------------------------------------- 1 | import { h, createApp } from 'vue'; 2 | import type { App } from 'vue'; 3 | 4 | import type { AddOnCustomProps } from '@scayle/add-on-utils'; 5 | 6 | import singleSpaVue from '@scayle/single-spa-vue'; 7 | 8 | import { initVuePlugins } from '@/vue-bootstrap'; 9 | import RootApp from './App.vue'; 10 | 11 | 12 | const vueLifecycles = singleSpaVue({ 13 | createApp, 14 | shadow: import.meta.env.PANEL_USE_SHADOW_DOM === 'true', 15 | appOptions: { 16 | render() { 17 | return h(RootApp); 18 | }, 19 | el: '#app', 20 | }, 21 | handleInstance(instance, props) { 22 | initVuePlugins(instance, { addonCustomProps: props }); 23 | }, 24 | }); 25 | 26 | export const { bootstrap, mount, unmount } = vueLifecycles; 27 | -------------------------------------------------------------------------------- /src/mocks/products.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | 5 | /** 6 | * Internal dependencies. 7 | */ 8 | import { Product } from '@/types/Product'; 9 | 10 | let products: Product[] = []; 11 | /** 12 | * We assume we load the products, before any module uses it 13 | * Do not do this on production, use an API and do the requests on queries or mutations 14 | */ 15 | import('@/mocks/products.json').then((response) => replaceProducts(response.default.data as Product[])) 16 | 17 | export const replaceProducts = (data: Product[]) => { 18 | products = [...data]; 19 | 20 | return products; 21 | } 22 | 23 | export const removeProducts = (ids: (Product['id'])[]) => { 24 | return replaceProducts( 25 | products.filter(product => !ids.includes(product.id)), 26 | ); 27 | } 28 | 29 | export default () => products; 30 | -------------------------------------------------------------------------------- /src/components/InputLabel/InputLabel.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | -------------------------------------------------------------------------------- /vitest-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | // This script is run to set up the environment for the unit tests 5 | // We want to mock the $addOn data in components and also provide 6 | // fake local and session storage APIs 7 | import Storage from 'dom-storage'; 8 | import { config } from '@vue/test-utils'; 9 | import { VueQueryPlugin } from '@tanstack/vue-query'; 10 | import { AddOnPropsPlugin, mockCustomProps } from '@scayle/add-on-utils'; 11 | 12 | /** 13 | * Internal dependencies. 14 | */ 15 | // @ts-ignore 16 | import router from './src/router'; 17 | 18 | (global as any).localStorage = new Storage(null, { strict: true }); 19 | (global as any).sessionStorage = new Storage(null, { strict: true }); 20 | 21 | config.global.plugins = [ 22 | [AddOnPropsPlugin, {customProps: mockCustomProps()}], 23 | [VueQueryPlugin], 24 | [router] 25 | ]; 26 | -------------------------------------------------------------------------------- /src/pages/FormPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | -------------------------------------------------------------------------------- /src/components/Statistics/Statistics.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 43 | -------------------------------------------------------------------------------- /src/composables/useDeleteProductsMutation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | 5 | import { useMutation, useQueryClient } from '@tanstack/vue-query'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import { startTimeout } from '@/utils'; 11 | import { Product } from '@/types/Product'; 12 | import { replaceProducts, removeProducts } from '@/mocks/products'; 13 | 14 | export default function useDeleteProductsMutation() { 15 | const queryClient = useQueryClient(); 16 | const { 17 | isLoading: isDeleting, 18 | mutateAsync: deleteProducts, 19 | } = useMutation({ 20 | mutationFn: async (ids: (Product['id'])[]) => { 21 | await startTimeout(1000); 22 | 23 | removeProducts(ids); 24 | }, 25 | onSuccess: () => { 26 | return queryClient.invalidateQueries(['products']); 27 | } 28 | }); 29 | 30 | return { 31 | isDeleting, 32 | deleteProducts, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ColumnTemplates/ProductStockColumnTemplate.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | -------------------------------------------------------------------------------- /src/components/ProductsListing/ProductsPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 42 | -------------------------------------------------------------------------------- /src/components/ProductsListing/PrdocutsListingBody/ProductsListingBody.css: -------------------------------------------------------------------------------- 1 | .el-table :deep(.el-table__cell) { 2 | @apply text-black; 3 | font-size: 13px; 4 | padding: 12px 10px; 5 | border-top: none; 6 | border-bottom: none; 7 | vertical-align: middle; 8 | } 9 | 10 | .el-table :deep(.el-scrollbar) { 11 | @apply overflow-auto; 12 | } 13 | 14 | .el-table :deep(.table-column-names-row th) { 15 | @apply bg-light text-black; 16 | 17 | border-top: 1px solid #e9eaec; 18 | border-bottom: none; 19 | font-size: 13px; 20 | font-weight: bold; 21 | padding: 5px 10px; 22 | } 23 | 24 | .el-table :deep(.table-column-names-row .cell) { 25 | @apply inline-flex; 26 | line-height: 1; 27 | } 28 | 29 | .el-table :deep(.table-column-names-row .cell .column-name-wrapper) { 30 | padding: 0; 31 | line-height: 1.5; 32 | width: 100%; 33 | } 34 | 35 | .el-table :deep(.table-column-names-row .cell .column-name-wrapper .column-name) { 36 | vertical-align: middle; 37 | padding: 0; 38 | line-height: 1.5; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ColumnTemplates/StringColumnTemplate.vue: -------------------------------------------------------------------------------- 1 | 9 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ABOUTYOU / Public 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "es2020", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "baseUrl": ".", 8 | "strict": true, 9 | "allowJs": true, 10 | "sourceMap": true, 11 | "noImplicitAny": true, 12 | "noImplicitThis": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "allowSyntheticDefaultImports": true, 16 | "paths": { 17 | "@/*": [ 18 | "./src/*" 19 | ] 20 | }, 21 | "lib": [ 22 | "ESNext", 23 | "DOM" 24 | ], 25 | "types": [ 26 | "unplugin-icons/types/vue", 27 | "vite/client" 28 | ], 29 | "noEmit": true, 30 | "skipLibCheck": true 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.d.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue" 37 | ], 38 | "exclude": [ 39 | "node_modules", 40 | "src/**/*.unit.ts" 41 | ], 42 | "references": [ 43 | { 44 | "path": "./tsconfig.node.json" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/TableListingDetail.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 49 | -------------------------------------------------------------------------------- /src/pages/DashboardPage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 50 | -------------------------------------------------------------------------------- /src/mocks/roles.json: -------------------------------------------------------------------------------- 1 | [{"id":1,"name":"Architect","userCount":63}, 2 | {"id":2,"name":"Construction Expeditor","userCount":10}, 3 | {"id":3,"name":"Surveyor","userCount":26}, 4 | {"id":4,"name":"Administrator","userCount":18}, 5 | {"id":5,"name":"Developer","userCount":82}, 6 | {"id":6,"name":"Developer Read Only","userCount":1}, 7 | {"id":7,"name":"Supervisor","userCount":87}, 8 | {"id":8,"name":"Estimator","userCount":23}, 9 | {"id":9,"name":"Key Account Manager","userCount":30}, 10 | {"id":10,"name":"Product Shop Manager","userCount":59}, 11 | {"id":11,"name":"Construction Foreman","userCount":63}, 12 | {"id":12,"name":"Electrician","userCount":90}, 13 | {"id":13,"name":"Engineer","userCount":66}, 14 | {"id":14,"name":"Marketing Manager","userCount":44}, 15 | {"id":15,"name":"Customer Manager","userCount":35}, 16 | {"id":16,"name":"Business Manager","userCount":25}, 17 | {"id":17,"name":"Quality Assurance","userCount":52}, 18 | {"id":18,"name":"Project Manager","userCount":3}, 19 | {"id":19,"name":"Campaign Marketing Manager","userCount":11}, 20 | {"id":20,"name":"Construction Manager","userCount":75}, 21 | {"id":21,"name":"Product Manager","userCount":92}, 22 | {"id":22,"name":"Cashier","userCount":69}, 23 | {"id":23,"name":"Designed","userCount":9}, 24 | {"id":24,"name":"Engineer","userCount":35}, 25 | {"id":25,"name":"Construction Worker","userCount":22}] 26 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 51 | -------------------------------------------------------------------------------- /src/vue-bootstrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import ScayleComponents from '@scayle/components'; 5 | import { VueQueryPlugin, VueQueryPluginOptions } from '@tanstack/vue-query'; 6 | import { AddOnCustomProps, AddOnPropsPlugin, mockCustomProps } from '@scayle/add-on-utils'; 7 | 8 | /** 9 | * Internal dependencies. 10 | */ 11 | import { App } from 'vue'; 12 | import { minutesToMilliseconds } from '@/utils'; 13 | import { ADDON_PROPERTIES_KEY } from '@/composables/useAddonProperties'; 14 | import router from './router'; 15 | 16 | export type InitVuePluginsOptions = { 17 | addonCustomProps?: AddOnCustomProps, 18 | }; 19 | 20 | export const initVuePlugins = (instance: App, { 21 | addonCustomProps 22 | }: InitVuePluginsOptions = {}) => { 23 | instance 24 | // Add fake add-on custom props when in standalone mode 25 | .use(AddOnPropsPlugin, {customProps: addonCustomProps || mockCustomProps()}) 26 | .use(router) 27 | .use(ScayleComponents) 28 | .use(VueQueryPlugin, { 29 | queryClientConfig: { 30 | defaultOptions: { 31 | queries: { 32 | refetchOnMount: false, // do not refetch queries on mount 33 | staleTime: minutesToMilliseconds(1) 34 | } 35 | } 36 | } 37 | } as VueQueryPluginOptions); 38 | 39 | instance.provide(ADDON_PROPERTIES_KEY, instance.config.globalProperties.$addOn); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ProductsListing/ProductsListing.css: -------------------------------------------------------------------------------- 1 | .products-listing { 2 | @apply flex flex-col w-full bg-white border relative overflow-hidden; 3 | border-radius: 5px; 4 | } 5 | 6 | .products-listing :deep(.products-listing__head) { 7 | @apply grid grid-cols-1 gap-2; 8 | 9 | padding: 15px 20px 15px 20px; 10 | } 11 | 12 | .products-listing :deep(.products-listing__head-inner) { 13 | @apply w-full flex justify-between items-center; 14 | } 15 | 16 | 17 | .products-listing :deep(.products-listing__meta) { 18 | @apply text-dark-grey py-2 -mb-4; 19 | 20 | border-top: 1px solid #e9eaec; 21 | } 22 | 23 | .products-listing :deep(.products-listing__filter) { 24 | @apply p-1 mr-2 bg-dark font-base outline-0 rounded; 25 | @apply inline-flex items-center; 26 | 27 | font-size: 13px; 28 | } 29 | 30 | .products-listing :deep(.products-listing__filter .icon) { 31 | @apply cursor-pointer; 32 | } 33 | 34 | :deep(.products-listing__head .ay-search .ay-search__input) { 35 | @apply accent-secondary h-[30px] rounded-full; 36 | } 37 | 38 | .products-listing :deep(.products-listing__body) { 39 | @apply flex flex-col justify-center overflow-hidden; 40 | } 41 | 42 | .products-listing__footer :deep(.ay-pagination-layout) { 43 | border: 0; 44 | } 45 | 46 | .products-listing__footer :deep(.ay-pagination-layout .ay-pagination-layout__group .ay-select) { 47 | @apply min-w-[130px] w-[130px]; 48 | } 49 | 50 | .products-listing__footer :deep(.ay-pagination-layout .ay-pagination-layout__group .ay-select__input) { 51 | @apply min-w-[130px] w-[130px]; 52 | } 53 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "enabledManagers": ["npm"], 7 | "rangeStrategy": "bump", 8 | "dependencyDashboard": false, 9 | "timezone": "Europe/Berlin", 10 | "gitLabIgnoreApprovals": true, 11 | "patch": { 12 | "prPriority": 30 13 | }, 14 | "minor": { 15 | "prPriority": 20 16 | }, 17 | "major": { 18 | "prPriority": 10 19 | }, 20 | "prConcurrentLimit": 12, 21 | "packageRules": [ 22 | // Minor/patch updates 23 | { 24 | "matchUpdateTypes": ["minor", "patch"], 25 | "labels": ["renovate-minor"], 26 | "prHourlyLimit": 1, 27 | "schedule": ["every weekend"], 28 | "rebaseWhen": "never", 29 | "groupName": "all non-major dependencies", 30 | "groupSlug": "all-minor-patch" 31 | }, 32 | // Major updates 33 | { 34 | "matchUpdateTypes": ["major"], 35 | "labels": ["renovate-major"], 36 | "schedule": null, 37 | "rebaseWhen": "never", 38 | "prHourlyLimit": 2 39 | }, 40 | // Auto-merge JS deps which won't affect the build (e.g. lint) 41 | { 42 | "matchPackagePrefixes": ["eslint", "@types"], 43 | "matchPackageNames": ["@vue/test-utils", "vitest", "@typescript-eslint/parser"], 44 | "automerge": true, 45 | "platformAutomerge": true, 46 | "labels": ["renovate-auto"], 47 | "schedule": null, 48 | "reviewers": [], 49 | "rebaseWhen": "auto", 50 | "prHourlyLimit": 2 51 | }, 52 | ], 53 | } 54 | -------------------------------------------------------------------------------- /src/components/ColumnTemplates/ImageColumnTemplate.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 53 | 54 | 75 | -------------------------------------------------------------------------------- /src/components/TopStatistics/TopStatistics.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 61 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { computed, ComputedRef, Ref } from 'vue'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | 10 | // TODO: Eventually these will be passed as an argument in the config function 11 | export const ADD_ON_ID = import.meta.env.PANEL_ADDON_IDENTIFIER; 12 | export const BASE_URL = `/add-ons/${ADD_ON_ID}#`; 13 | 14 | export const generateGroupName = (name: string) => ADD_ON_ID + '::' + name; 15 | 16 | export const startTimeout = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)); 17 | 18 | export const getInitials = function (value: string) { 19 | const names = value.split(' '); 20 | let initials = names[0].substring(0, 1).toUpperCase(); 21 | 22 | if (names.length > 1) { 23 | initials += names[names.length - 1].substring(0, 1).toUpperCase(); 24 | } 25 | 26 | return initials; 27 | }; 28 | 29 | export const firstLetterToUpper = (value: string) => value.slice(0, 1).toUpperCase() + value.slice(1); 30 | 31 | export const wrapInRef = (refObject: Ref | undefined, defaultValue: T): ComputedRef => { 32 | if (refObject === undefined) { 33 | return computed(() => defaultValue); 34 | } 35 | 36 | return computed(() => refObject.value); 37 | } 38 | 39 | export const daysToSeconds = (days: number) => hoursToSeconds(days * 24); 40 | 41 | export const hoursToSeconds = (hours: number) => minutesToSeconds(hours * 60); 42 | 43 | export const minutesToSeconds = (minutes: number) => minutes * 60; 44 | 45 | export const hoursToMilliseconds = (hours: number) => secondsToMilliseconds(hoursToSeconds(hours)); 46 | 47 | export const minutesToMilliseconds = (minutes: number) => secondsToMilliseconds(minutesToSeconds(minutes)); 48 | 49 | export const secondsToMilliseconds = (seconds: number) => seconds * 1000; 50 | -------------------------------------------------------------------------------- /src/components/ImagePlaceholderWrapper/ImagePlaceholderWrapper.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 72 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Panel Add-On 6 | 7 | 8 | 9 |
10 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/components/ColumnTemplates/ProductActionsColumnTemplate.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 80 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: false, 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | plugins: ['unicorn'], 9 | extends: [ 10 | 'plugin:import/errors', 11 | 'plugin:import/warnings', 12 | 'plugin:vue/vue3-recommended', 13 | 'eslint:recommended', 14 | ], 15 | settings: { 16 | 'import/resolver': { 17 | typescript: {}, 18 | }, 19 | }, 20 | parser: 'vue-eslint-parser', 21 | parserOptions: { 22 | parser: { 23 | ts: '@typescript-eslint/parser', 24 | }, 25 | sourceType: 'module', 26 | }, 27 | rules: { 28 | 'vue/require-explicit-emits': [ 29 | 'error', 30 | { 31 | allowProps: false, 32 | }, 33 | ], 34 | 'vue/attributes-order': 'error', 35 | 'vue/multi-word-component-names': 'off', 36 | 'vue/component-name-in-template-casing': ['error', 'PascalCase'], 37 | 'vue/html-self-closing': ['error', { 38 | 'html': { 39 | 'void': 'always', 40 | 'normal': 'always', 41 | 'component': 'always' 42 | }, 43 | 'svg': 'always', 44 | 'math': 'always' 45 | }], 46 | 'vue/html-closing-bracket-spacing': [ 47 | 'error', 48 | { 49 | startTag: 'never', 50 | endTag: 'never', 51 | selfClosingTag: 'always', 52 | }, 53 | ], 54 | // eslint-plugin-import 55 | 'import/first': 'error', 56 | 'import/order': [ 57 | 'error', 58 | { 59 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'], 60 | }, 61 | ], 62 | 'import/no-mutable-exports': 'error', 63 | 'import/no-unresolved': 'off', 64 | 'no-unused-vars': ['off'], 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /src/components/ColumnTemplates/StatusColumnTemplate.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 74 | 75 | 78 | -------------------------------------------------------------------------------- /server-ssl/generate-ssl.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | which openssl 4 | 5 | if [ "$?" -ne 0 ]; then 6 | echo "You do not have openssl CLI command installed" 7 | exit 1 8 | fi 9 | 10 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 11 | ROOT_DIR=$SCRIPT_DIR/../ 12 | 13 | cd "$SCRIPT_DIR" 14 | 15 | if [ -f "$ROOT_DIR"/.env ]; then 16 | source "$ROOT_DIR"/.env 17 | fi 18 | 19 | if [ -z "$CONFIG_SERVER_HOST" ]; then 20 | SERVER_HOST=demo-add-on.cloud-panel.aboutyou.test 21 | else 22 | SERVER_HOST=$CONFIG_SERVER_HOST 23 | fi 24 | 25 | cp default.conf temp.conf 26 | 27 | case "$(uname -sr)" in 28 | Darwin*) 29 | sed -i '' "s/{{SERVER_HOST}}/$SERVER_HOST/g" temp.conf 30 | ;; 31 | 32 | *) 33 | sed -i "s/{{SERVER_HOST}}/$SERVER_HOST/g" temp.conf 34 | ;; 35 | esac 36 | 37 | openssl genrsa -out default.key 2048 38 | openssl req -new -x509 -key default.key -out default.crt -days 3650 -subj /CN=$SERVER_HOST -extensions SAN -config temp.conf 39 | 40 | echo "Generated certificate for: $SERVER_HOST" 41 | 42 | case "$(uname -sr)" in 43 | 44 | Darwin*) 45 | sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain default.crt 46 | ;; 47 | 48 | Linux*) 49 | source /etc/os-release 50 | if [ "$ID_LIKE" == "debian" ] || [ "$ID" == "ubuntu" ]; then 51 | apt-get install -y ca-certificates 52 | sudo cp default.crt /usr/local/share/ca-certificates/default.crt 53 | sudo update-ca-certificates 54 | elif [ "$ID" == "fedora" ]; then 55 | trust anchor default.crt 56 | update-ca-trust 57 | else 58 | echo "Your OS is not supported. You will have to trust your certificate by yourself" 59 | fi 60 | ;; 61 | 62 | CYGWIN*|MINGW32*|MINGW*|MSYS*) 63 | certutil -addstore -f "ROOT" new-root-certificate.crt 64 | ;; 65 | 66 | *) 67 | echo "Your OS is not supported. You will have to trust your certificate by yourself" 68 | ;; 69 | esac 70 | 71 | rm temp.conf 72 | 73 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs/Breadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 77 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | // This config function does nothing but return its input 2 | // But it does provide some helpful type hints 3 | import { config, RouteDefinition } from '@scayle/add-on-utils'; 4 | import { AddOnRoute, routes } from './routes'; 5 | import { GroupRouteDefinition } from './types'; 6 | import { ADD_ON_ID, generateGroupName } from './utils'; 7 | 8 | const applyDefaultRouteProps = (routes: RouteDefinition[]) => routes.map(route => ({...route, sidebar: route.sidebar === null ? null : ADD_ON_ID})); 9 | 10 | const mappedRoutes = routes.map(originalRoute => { 11 | const route = JSON.parse(JSON.stringify(originalRoute)) as AddOnRoute; 12 | const children = route.children; 13 | const meta = route.meta as RouteDefinition; 14 | 15 | if(children && children.length && !meta.children) { 16 | meta.children = children.map(childRoute => { 17 | const pathArray = childRoute.path.split("/"); 18 | const pathForManifest = pathArray[pathArray.length - 1]; 19 | 20 | // if the path for the manifest also contains the path of the parent, 21 | // the active item highlighting in the sidebar does not work that's what the lines above are for 22 | 23 | return "/" + pathForManifest; 24 | }); 25 | } 26 | 27 | return meta; 28 | }); 29 | 30 | const generalRoutes: GroupRouteDefinition[] = [ 31 | { 32 | id: 'general-group', 33 | name: { 34 | 'en': 'General', 35 | 'de': 'Allgemeines' 36 | }, 37 | group: generateGroupName('general'), 38 | isGroup: true, 39 | }, 40 | ...mappedRoutes 41 | ]; 42 | 43 | const manifestRegistration = config(function (registerApplication, registerRoutes) { 44 | registerApplication({ 45 | name: ADD_ON_ID, 46 | // Make sure to use a dynamic import to create a code-split point 47 | // and minimize the size of the manifest since it is loaded on every page 48 | app: () => import('./add-on'), 49 | }); 50 | 51 | registerRoutes({ 52 | [ADD_ON_ID]: [ 53 | ...applyDefaultRouteProps(generalRoutes as RouteDefinition[]), 54 | ] 55 | }) 56 | }); 57 | 58 | // !!!!!DO NOT CHANGE LINE BELOW 59 | // IT IS USED FOR HOT RELOADING ON CLOUD PANEL ADDON 60 | // AND WILL BREAK IF THE LINE BELOW IS CHANGED 61 | export default manifestRegistration; 62 | -------------------------------------------------------------------------------- /src/components/ProductsListing/PrdocutsListingBody/ProductsListingBody.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 87 | 88 |