@userfrosting/sprinkle-core 6.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2013-2024 Alexander Weissman & Louis Charette
2
+
3
+ UserFrosting is 100% free and open-source.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # UserFrosting 5.2 Core Sprinkle
2
+
3
+ [![Version](https://img.shields.io/github/v/release/userfrosting/sprinkle-core?include_prereleases)](https://github.com/userfrosting/sprinkle-core/releases)
4
+ ![PHP Version](https://img.shields.io/badge/php-%5E8.1-brightgreen)
5
+ [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md)
6
+ [![Build](https://img.shields.io/github/actions/workflow/status/userfrosting/sprinkle-core/Build.yml?branch=5.2&logo=github)](https://github.com/userfrosting/sprinkle-core/actions)
7
+ [![Codecov](https://codecov.io/gh/userfrosting/sprinkle-core/branch/5.2/graph/badge.svg)](https://app.codecov.io/gh/userfrosting/sprinkle-core/branch/5.2)
8
+ [![StyleCI](https://github.styleci.io/repos/372359383/shield?branch=5.2&style=flat)](https://github.styleci.io/repos/372359383)
9
+ [![PHPStan](https://img.shields.io/github/actions/workflow/status/userfrosting/sprinkle-core/PHPStan.yml?branch=5.2&label=PHPStan)](https://github.com/userfrosting/sprinkle-core/actions/workflows/PHPStan.yml)
10
+ [![Join the chat](https://img.shields.io/badge/Chat-UserFrosting-brightgreen?logo=Rocket.Chat)](https://chat.userfrosting.com)
11
+ [![Donate](https://img.shields.io/badge/Open_Collective-Donate-blue?logo=Open%20Collective)](https://opencollective.com/userfrosting#backer)
12
+ [![Donate](https://img.shields.io/badge/Ko--fi-Donate-blue?logo=ko-fi&logoColor=white)](https://ko-fi.com/lcharette)
13
+
14
+ ## By [Alex Weissman](https://alexanderweissman.com) and [Louis Charette](https://bbqsoftwares.com)
15
+
16
+ Copyright (c) 2013-2024, free to use in personal and commercial software as per the [license](LICENSE.md).
17
+
18
+ UserFrosting is a secure, modern user management system written in PHP and built on top of the [Slim Microframework](http://www.slimframework.com/), [Twig](http://twig.sensiolabs.org/) templating engine, and [Eloquent](https://laravel.com/docs/10.x/eloquent#introduction) ORM.
19
+
20
+ This **Core Sprinkle** provides most of the "heavy lifting" PHP code. It provides all the necessary services for database, templating, error handling, mail support, request throttling, and more.
21
+
22
+ ## Installation in your UserFrosting project
23
+ To use this sprinkle in your UserFrosting project, follow theses instructions (*N.B.: This sprinkle is enabled by default when using the base app template*).
24
+
25
+ 1. Require in your [UserFrosting](https://github.com/userfrosting/UserFrosting) project :
26
+ ```
27
+ composer require userfrosting/sprinkle-core
28
+ ```
29
+
30
+ 2. Add the Sprinkle to your Sprinkle Recipe :
31
+ ```php
32
+ public function getSprinkles(): array
33
+ {
34
+ return [
35
+ \UserFrosting\Sprinkle\Core\Core::class,
36
+ ];
37
+ }
38
+ ```
39
+
40
+ 3. Bake
41
+ ```bash
42
+ php bakery bake
43
+ ```
44
+
45
+ ## Install locally and run tests
46
+ You can also install this sprinkle locally. This can be useful to debug or contribute to this sprinkle.
47
+
48
+ 1. Clone repo :
49
+ ```
50
+ git clone https://github.com/userfrosting/sprinkle-core.git
51
+ ```
52
+ 2. Change directory
53
+ ```
54
+ cd sprinkle-core
55
+ ```
56
+ 3. Install dependencies :
57
+ ```
58
+ composer install
59
+ ```
60
+ 4. Run bake command :
61
+ ```
62
+ php bakery bake
63
+ ```
64
+
65
+ From this point, you can use the same command as with any other sprinkle.
66
+
67
+ Tests can be run using the bundled PHPUnit :
68
+ ```
69
+ vendor/bin/phpunit
70
+ ```
71
+
72
+ Same for PHPStan, for code quality :
73
+ ```
74
+ vendor/bin/phpstan analyse app/src/
75
+ ```
76
+
77
+ ## Documentation
78
+ See main [UserFrosting Documentation](https://learn.userfrosting.com) for more information.
79
+
80
+ - [Changelog](CHANGELOG.md)
81
+ - [Issues](https://github.com/userfrosting/UserFrosting/issues)
82
+ - [License](LICENSE.md)
83
+ - [Style Guide](https://github.com/userfrosting/.github/blob/main/.github/STYLE-GUIDE.md)
84
+
85
+ ## Contributing
86
+
87
+ This project exists thanks to all the people who contribute. If you're interested in contributing to the UserFrosting codebase, please see our [contributing guidelines](https://github.com/userfrosting/UserFrosting/blob/5.2/.github/CONTRIBUTING.md) as well as our [style guidelines](.github/STYLE-GUIDE.md).
88
+
89
+ [![](https://opencollective.com/userfrosting/contributors.svg?width=890&button=true)](https://github.com/userfrosting/sprinkle-core/graphs/contributors)
@@ -0,0 +1 @@
1
+ export { useSprunjer } from './sprunjer'
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Sprunjer Composable
3
+ *
4
+ * A composable function that fetches data from a Sprunjer API and provides
5
+ * all necessary function like pagination, sorting and filtering.
6
+ *
7
+ * Pass the URL of the Sprunjer API to the function, and it will fetch the data.
8
+ * A watcher will refetch the data whenever any parameters change.
9
+ *
10
+ * Params:
11
+ * @param {String} dataUrl - The URL of the Sprunjer API
12
+ * @param {Object} defaultSorts - An object of default sorts
13
+ * @param {Object} defaultFilters - An object of default filters
14
+ * @param {Number} defaultSize - The default number of items per page
15
+ * @param {Number} defaultPage - The default page number
16
+ *
17
+ * Exports:
18
+ * - size: The number of items per page
19
+ * - page: The current page number
20
+ * - sorts: An object of sorts
21
+ * - filters: An object of filters
22
+ * - data: The raw data from the API
23
+ * - fetch: A function to fetch the data
24
+ * - loading: A boolean indicating if the data is loading
25
+ * - totalPages: The total number of pages
26
+ * - downloadCsv: A function to download the data as a CSV file
27
+ * - countFiltered: The total number of items after filtering
28
+ * - count: The total number of items
29
+ * - rows: The rows of data
30
+ * - first: The index of the first item on the current page
31
+ * - last: The index of the last item on the current page
32
+ * - toggleSort: A function to toggle the sort order of a column
33
+ */
34
+ import { ref, toValue, watchEffect, computed } from 'vue'
35
+ import axios from 'axios'
36
+ import type { AssociativeArray, Sprunjer } from '../interfaces'
37
+
38
+ export const useSprunjer = (
39
+ dataUrl: any,
40
+ defaultSorts: AssociativeArray = {},
41
+ defaultFilters: AssociativeArray = {},
42
+ defaultSize: number = 10,
43
+ defaultPage: number = 0
44
+ ): Sprunjer => {
45
+ // Sprunje parameters
46
+ const size = ref<number>(defaultSize)
47
+ const page = ref<number>(defaultPage)
48
+ const sorts = ref<AssociativeArray>(defaultSorts)
49
+ const filters = ref<AssociativeArray>(defaultFilters)
50
+
51
+ // Raw data
52
+ const data = ref<any>({})
53
+
54
+ // State
55
+ const loading = ref<boolean>(false)
56
+
57
+ /**
58
+ * Api fetch function
59
+ */
60
+ async function fetch() {
61
+ loading.value = true
62
+ axios
63
+ .get(toValue(dataUrl), {
64
+ params: {
65
+ size: size.value,
66
+ page: page.value,
67
+ sorts: sorts.value,
68
+ filters: filters.value
69
+ }
70
+ })
71
+ .then((response) => {
72
+ data.value = response.data
73
+ loading.value = false
74
+ })
75
+ .catch((err) => {
76
+ // TODO : User toast alert, or export alert
77
+ console.error(err)
78
+ })
79
+ }
80
+
81
+ /**
82
+ * Computed properties
83
+ */
84
+ const totalPages = computed(() => {
85
+ // N.B.: Sprunjer page starts at 0, not 1
86
+ // Make sure page is never negative
87
+ return Math.max(Math.ceil((data.value.count_filtered ?? 0) / size.value) - 1, 0)
88
+ })
89
+
90
+ const count = computed(() => {
91
+ return data.value.count ?? 0
92
+ })
93
+
94
+ const first = computed(() => {
95
+ return Math.min(page.value * size.value + 1, data.value.count ?? 0)
96
+ })
97
+
98
+ const last = computed(() => {
99
+ return Math.min((page.value + 1) * size.value, data.value.count_filtered ?? 0)
100
+ })
101
+
102
+ const countFiltered = computed(() => {
103
+ return data.value.count_filtered ?? 0
104
+ })
105
+
106
+ const rows = computed(() => {
107
+ return data.value.rows ?? []
108
+ })
109
+
110
+ /**
111
+ * Download the data as a CSV file
112
+ */
113
+ function downloadCsv() {
114
+ console.log('Not yet implemented')
115
+ }
116
+
117
+ /**
118
+ * Apply sorting to a column, cycling from the previous sort order.
119
+ * Order goes : asc -> desc -> null -> asc
120
+ * Used to toggle the sort order of a column when the column header is clicked
121
+ * @param column The column to sort
122
+ */
123
+ function toggleSort(column: string) {
124
+ let newOrder: string | null
125
+ if (sorts.value[column] === 'asc') {
126
+ newOrder = 'desc'
127
+ } else if (sorts.value[column] === 'desc') {
128
+ newOrder = null
129
+ } else {
130
+ newOrder = 'asc'
131
+ }
132
+
133
+ sorts.value[column] = newOrder
134
+ }
135
+
136
+ /**
137
+ * Automatically fetch the data when any parameters change
138
+ */
139
+ watchEffect(() => {
140
+ fetch()
141
+ })
142
+
143
+ /**
144
+ * Export the functions and data
145
+ */
146
+ return {
147
+ dataUrl,
148
+ size,
149
+ page,
150
+ sorts,
151
+ filters,
152
+ data,
153
+ fetch,
154
+ loading,
155
+ downloadCsv,
156
+ totalPages,
157
+ countFiltered,
158
+ count,
159
+ rows,
160
+ first,
161
+ last,
162
+ toggleSort
163
+ }
164
+ }
@@ -0,0 +1,8 @@
1
+ import { useConfigStore } from './stores'
2
+
3
+ export default {
4
+ install: () => {
5
+ const config = useConfigStore()
6
+ config.load()
7
+ }
8
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Alert Interface
3
+ *
4
+ * Represents a common interface for alert components. This interface is used by
5
+ * API when an error occurs or a successful event occurs, and consumed by the
6
+ * interface.
7
+ */
8
+ import { Severity } from './severity'
9
+
10
+ export interface AlertInterface {
11
+ title?: string
12
+ description?: string
13
+ style?: Severity | keyof typeof Severity
14
+ closeBtn?: boolean
15
+ hideIcon?: boolean
16
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Common Interfaces
3
+ *
4
+ * Returns miscellaneous interfaces that are used throughout the application.
5
+ */
6
+ export interface AssociativeArray {
7
+ [key: string]: string | null
8
+ }
@@ -0,0 +1,4 @@
1
+ export type { AlertInterface } from './alerts'
2
+ export type { AssociativeArray } from './common'
3
+ export { Severity } from './severity'
4
+ export type { Sprunjer } from './sprunjer'
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Severity Enum
3
+ *
4
+ * This is a shared enum used to define the severity of different components
5
+ * (e.g., alert, button, etc.). This makes it easier to reference severity
6
+ * levels across multiple components, also defining a common concept across
7
+ * themes.
8
+ *
9
+ * Template components must accept all values of this enum as valid input.
10
+ * However, themes may choose to ignore or bind some values to another style.
11
+ * For example, a theme might not have an 'Info' colored button. In this case,
12
+ * the theme's button component must accept 'Info' as a valid input, but it can
13
+ * map it to the 'Primary' style.
14
+ */
15
+ export enum Severity {
16
+ Primary = 'Primary',
17
+ Secondary = 'Secondary',
18
+ Success = 'Success',
19
+ Warning = 'Warning',
20
+ Danger = 'Danger',
21
+ Info = 'Info',
22
+ Muted = 'Muted', // Aka, Disabled
23
+ Default = 'Default' // No-style or default style
24
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Sprunjer Interface
3
+ *
4
+ * Represents the interface for the Sprunjer composable.
5
+ */
6
+ import type { Ref, ComputedRef } from 'vue'
7
+ import type { AssociativeArray } from '.'
8
+
9
+ export interface Sprunjer {
10
+ dataUrl: any
11
+ size: Ref<number>
12
+ page: Ref<number>
13
+ sorts: Ref<AssociativeArray>
14
+ filters: Ref<AssociativeArray>
15
+ data: Ref<any>
16
+ fetch: () => void
17
+ loading: Ref<boolean>
18
+ totalPages: ComputedRef<number>
19
+ downloadCsv: () => void
20
+ countFiltered: ComputedRef<number>
21
+ count: ComputedRef<number>
22
+ rows: ComputedRef<any[]>
23
+ first: ComputedRef<number>
24
+ last: ComputedRef<number>
25
+ toggleSort: (column: string) => void
26
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Config Store
3
+ *
4
+ * This store is used to access the configuration of the application from the
5
+ * API.
6
+ */
7
+ import { defineStore } from 'pinia'
8
+ import axios from 'axios'
9
+ import { getProperty } from 'dot-prop'
10
+
11
+ export const useConfigStore = defineStore('config', {
12
+ persist: true,
13
+ state: () => {
14
+ return {
15
+ config: {}
16
+ }
17
+ },
18
+ getters: {
19
+ get: (state) => {
20
+ return (key: string, value?: any): any => getProperty(state.config, key, value)
21
+ }
22
+ },
23
+ actions: {
24
+ async load() {
25
+ axios.get('/api/config').then((response) => {
26
+ this.config = response.data
27
+ })
28
+ }
29
+ }
30
+ })
@@ -0,0 +1 @@
1
+ export { useConfigStore } from './config'
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { type AlertInterface, Severity } from '../../interfaces'
3
+
4
+ describe('AlertInterface', () => {
5
+ test('should create an alert with title and description', () => {
6
+ const alert: AlertInterface = {
7
+ title: 'Test Alert',
8
+ description: 'This is a test alert'
9
+ }
10
+
11
+ expect(alert.title).toBe('Test Alert')
12
+ expect(alert.description).toBe('This is a test alert')
13
+ })
14
+
15
+ test('should create an alert with style', () => {
16
+ const alert: AlertInterface = {
17
+ style: Severity.Success
18
+ }
19
+
20
+ expect(alert.style).toBe(Severity.Success)
21
+ })
22
+
23
+ test('should create an alert with close button and hide icon', () => {
24
+ const alert: AlertInterface = {
25
+ closeBtn: true,
26
+ hideIcon: true
27
+ }
28
+
29
+ expect(alert.closeBtn).toBe(true)
30
+ expect(alert.hideIcon).toBe(true)
31
+ })
32
+ })
33
+
34
+ describe('Severity', () => {
35
+ test('should have the correct values', () => {
36
+ expect(Severity.Primary).toBe('Primary')
37
+ expect(Severity.Secondary).toBe('Secondary')
38
+ expect(Severity.Success).toBe('Success')
39
+ expect(Severity.Warning).toBe('Warning')
40
+ expect(Severity.Danger).toBe('Danger')
41
+ expect(Severity.Info).toBe('Info')
42
+ })
43
+ })
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import { useConfigStore } from '../stores/config'
3
+ import plugin from '..'
4
+ import * as Config from '../stores/config'
5
+
6
+ const mockConfigStore = {
7
+ load: vi.fn()
8
+ }
9
+
10
+ describe('Plugin', () => {
11
+ test('should install the plugin and initiate load', () => {
12
+ vi.spyOn(Config, 'useConfigStore').mockReturnValue(mockConfigStore as any)
13
+
14
+ plugin.install()
15
+
16
+ expect(useConfigStore).toHaveBeenCalled()
17
+ expect(mockConfigStore.load).toHaveBeenCalled()
18
+ })
19
+ })
@@ -0,0 +1,42 @@
1
+ import { describe, expect, beforeEach, test, vi } from 'vitest'
2
+ import { setActivePinia, createPinia } from 'pinia'
3
+ import axios from 'axios'
4
+ import { useConfigStore } from '../../stores/config'
5
+
6
+ const testConfig = {
7
+ name: 'Test Config',
8
+ description: 'Test description',
9
+ api: {
10
+ url: 'https://api.example.com',
11
+ version: '1.0'
12
+ }
13
+ }
14
+
15
+ describe('Config Store', () => {
16
+ beforeEach(() => {
17
+ setActivePinia(createPinia())
18
+ })
19
+
20
+ test('should load config data', async () => {
21
+ // Arrange
22
+ const configStore = useConfigStore()
23
+ const response = { data: testConfig }
24
+ vi.spyOn(axios, 'get').mockResolvedValue(response as any)
25
+
26
+ // Assert initial state
27
+ expect(configStore.config).toEqual({})
28
+
29
+ // Act
30
+ await configStore.load()
31
+
32
+ // Assert
33
+ expect(axios.get).toHaveBeenCalledWith('/api/config')
34
+ expect(configStore.config).toStrictEqual(testConfig)
35
+
36
+ // Assert get method
37
+ expect(configStore.get('name')).toBe('Test Config')
38
+ expect(configStore.get('api.url')).toBe('https://api.example.com')
39
+ expect(configStore.get('api.version', '0.0')).toBe('1.0')
40
+ expect(configStore.get('api.key', 'API_KEY')).toBe('API_KEY')
41
+ })
42
+ })
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@userfrosting/sprinkle-core",
3
+ "version": "6.0.0-alpha.1",
4
+ "type": "module",
5
+ "description": "Core Sprinkle for UserFrosting",
6
+ "funding": "https://opencollective.com/userfrosting",
7
+ "license": "MIT",
8
+ "author": "Louis Charette (https://bbqsoftwares.com/)",
9
+ "contributors": [
10
+ "Alexander Weissman (https://alexanderweissman.com/)",
11
+ "Louis Charette (https://bbqsoftwares.com/)"
12
+ ],
13
+ "keywords": [
14
+ "UserFrosting",
15
+ "Core",
16
+ "Sprinkle"
17
+ ],
18
+ "homepage": "https://github.com/userfrosting/sprinkle-core#readme",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/userfrosting/sprinkle-core.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/userfrosting/UserFrosting/issues"
25
+ },
26
+ "exports": {
27
+ ".": "./app/assets/index.ts",
28
+ "./interfaces": "./app/assets/interfaces/index.ts",
29
+ "./stores": "./app/assets/stores/index.ts",
30
+ "./composables": "./app/assets/composables/index.ts"
31
+ },
32
+ "files": [
33
+ "app/assets/"
34
+ ],
35
+ "dependencies": {
36
+ "dot-prop": "^9.0.0"
37
+ },
38
+ "peerDependencies": {
39
+ "axios": "^1.5.0",
40
+ "pinia": "^2.1.6",
41
+ "pinia-plugin-persistedstate": "^3.2.0",
42
+ "vue": "^3.4.21"
43
+ },
44
+ "devDependencies": {
45
+ "@rushstack/eslint-patch": "^1.8.0",
46
+ "@tsconfig/node20": "^20.1.4",
47
+ "@types/node": "^20.12.5",
48
+ "@vitejs/plugin-vue": "^5.0.4",
49
+ "@vitest/coverage-v8": "^1.6.0",
50
+ "@vue/eslint-config-prettier": "^9.0.0",
51
+ "@vue/eslint-config-typescript": "^13.0.0",
52
+ "@vue/test-utils": "^2.4.6",
53
+ "@vue/tsconfig": "^0.5.1",
54
+ "eslint": "^8.57.0",
55
+ "eslint-plugin-vue": "^9.23.0",
56
+ "happy-dom": "^15.11.6",
57
+ "less": "^4.2.0",
58
+ "npm-run-all2": "^6.1.2",
59
+ "prettier": "^3.2.5",
60
+ "vite": "^5.2.8",
61
+ "vite-plugin-dts": "^4.0.0",
62
+ "vitest": "^1.6.0",
63
+ "vue": "^3.4.21",
64
+ "vue-router": "^4.2.4",
65
+ "vue-tsc": "^2.0.11"
66
+ },
67
+ "scripts": {
68
+ "dev": "vite",
69
+ "typecheck": "vue-tsc --noEmit",
70
+ "build": "vue-tsc && vite build",
71
+ "lint": "eslint app/assets/ --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
72
+ "format": "prettier --write app/assets/",
73
+ "test": "vitest",
74
+ "coverage": "vitest run --coverage"
75
+ }
76
+ }