mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat: Add reusable frontend composables package (#13077)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>
This commit is contained in:
3
packages/frontend/@n8n/composables/.eslintrc.cjs
Normal file
3
packages/frontend/@n8n/composables/.eslintrc.cjs
Normal file
@@ -0,0 +1,3 @@
|
||||
const { createFrontendEslintConfig } = require('@n8n/frontend-eslint-config');
|
||||
|
||||
module.exports = createFrontendEslintConfig(__dirname);
|
||||
24
packages/frontend/@n8n/composables/.gitignore
vendored
Normal file
24
packages/frontend/@n8n/composables/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
24
packages/frontend/@n8n/composables/README.md
Normal file
24
packages/frontend/@n8n/composables/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# @n8n/composables
|
||||
|
||||
A collection of Vue composables that provide common functionality across n8n's Front-End packages.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
- **Reusable Logic**: Encapsulate complex stateful logic into composable functions.
|
||||
- **Consistency**: Ensure consistent patterns and practices across our Vue components.
|
||||
- **Extensible**: Easily add new composables as our project grows.
|
||||
- **Optimized**: Fully compatible with the Composition API.
|
||||
|
||||
## Contributing
|
||||
|
||||
For more details, please read our [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
For more details, please read our [LICENSE.md](LICENSE.md).
|
||||
4
packages/frontend/@n8n/composables/biome.jsonc
Normal file
4
packages/frontend/@n8n/composables/biome.jsonc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["../../../../biome.jsonc"]
|
||||
}
|
||||
46
packages/frontend/@n8n/composables/package.json
Normal file
46
packages/frontend/@n8n/composables/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@n8n/composables",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
"./*": {
|
||||
"types": "./dist/*.d.ts",
|
||||
"import": "./dist/*.js",
|
||||
"require": "./dist/*.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "pnpm run typecheck && tsup",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"lint": "eslint src --ext .js,.ts,.vue --quiet",
|
||||
"lintfix": "eslint src --ext .js,.ts,.vue --fix",
|
||||
"format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore",
|
||||
"format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "catalog:frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/frontend-eslint-config": "workspace:*",
|
||||
"@n8n/frontend-typescript-config": "workspace:*",
|
||||
"@n8n/frontend-vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "catalog:frontend",
|
||||
"@testing-library/user-event": "catalog:frontend",
|
||||
"@testing-library/vue": "catalog:frontend",
|
||||
"@vitejs/plugin-vue": "catalog:frontend",
|
||||
"@vue/tsconfig": "catalog:frontend",
|
||||
"tsup": "catalog:frontend",
|
||||
"typescript": "catalog:frontend",
|
||||
"vite": "catalog:frontend",
|
||||
"vite-plugin-dts": "catalog:frontend",
|
||||
"vitest": "catalog:frontend",
|
||||
"vue-tsc": "catalog:frontend"
|
||||
},
|
||||
"license": "See LICENSE.md file in the root of the repository"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { configure } from '@testing-library/vue';
|
||||
|
||||
configure({ testIdAttribute: 'data-test-id' });
|
||||
1
packages/frontend/@n8n/composables/src/shims.d.ts
vendored
Normal file
1
packages/frontend/@n8n/composables/src/shims.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
125
packages/frontend/@n8n/composables/src/useDeviceSupport.test.ts
Normal file
125
packages/frontend/@n8n/composables/src/useDeviceSupport.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useDeviceSupport } from './useDeviceSupport';
|
||||
|
||||
const detectPointerType = (query: string) => {
|
||||
const isCoarse = query === '(any-pointer: coarse)';
|
||||
const isFine = query === '(any-pointer: fine)';
|
||||
return { fine: isFine, coarse: isCoarse };
|
||||
};
|
||||
|
||||
describe('useDeviceSupport()', () => {
|
||||
beforeEach(() => {
|
||||
global.window = Object.create(window);
|
||||
global.navigator = { userAgent: 'test-agent', maxTouchPoints: 0 } as Navigator;
|
||||
});
|
||||
|
||||
describe('isTouchDevice', () => {
|
||||
it('should be false if window matches `any-pointer: fine` and `!any-pointer: coarse`', () => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
value: vi.fn().mockImplementation((query: string) => {
|
||||
const { fine, coarse } = detectPointerType(query);
|
||||
return { matches: fine && !coarse };
|
||||
}),
|
||||
});
|
||||
const { isTouchDevice } = useDeviceSupport();
|
||||
expect(isTouchDevice).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be false if window matches `any-pointer: fine` and `any-pointer: coarse`', () => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
value: vi.fn().mockImplementation((query: string) => {
|
||||
const { fine, coarse } = detectPointerType(query);
|
||||
return { matches: fine && coarse };
|
||||
}),
|
||||
});
|
||||
const { isTouchDevice } = useDeviceSupport();
|
||||
expect(isTouchDevice).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be true if window matches `any-pointer: coarse` and `!any-pointer: fine`', () => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
value: vi.fn().mockImplementation((query: string) => {
|
||||
const { fine, coarse } = detectPointerType(query);
|
||||
return { matches: coarse && !fine };
|
||||
}),
|
||||
});
|
||||
const { isTouchDevice } = useDeviceSupport();
|
||||
expect(isTouchDevice).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMacOs', () => {
|
||||
it('should be true for macOS user agent', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' });
|
||||
const { isMacOs } = useDeviceSupport();
|
||||
expect(isMacOs).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be false for non-macOS user agent', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'windows' });
|
||||
const { isMacOs } = useDeviceSupport();
|
||||
expect(isMacOs).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('controlKeyCode', () => {
|
||||
it('should return Meta on macOS', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' });
|
||||
const { controlKeyCode } = useDeviceSupport();
|
||||
expect(controlKeyCode).toEqual('Meta');
|
||||
});
|
||||
|
||||
it('should return Control on non-macOS', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'windows' });
|
||||
const { controlKeyCode } = useDeviceSupport();
|
||||
expect(controlKeyCode).toEqual('Control');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMobileDevice', () => {
|
||||
it('should be true for iOS user agent', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'iphone' });
|
||||
const { isMobileDevice } = useDeviceSupport();
|
||||
expect(isMobileDevice).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be true for Android user agent', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'android' });
|
||||
const { isMobileDevice } = useDeviceSupport();
|
||||
expect(isMobileDevice).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be false for non-mobile user agent', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'windows' });
|
||||
const { isMobileDevice } = useDeviceSupport();
|
||||
expect(isMobileDevice).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be true for iPad user agent', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'ipad' });
|
||||
const { isMobileDevice } = useDeviceSupport();
|
||||
expect(isMobileDevice).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be true for iPod user agent', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'ipod' });
|
||||
const { isMobileDevice } = useDeviceSupport();
|
||||
expect(isMobileDevice).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCtrlKeyPressed()', () => {
|
||||
it('should return true for metaKey press on macOS', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' });
|
||||
const { isCtrlKeyPressed } = useDeviceSupport();
|
||||
const event = new KeyboardEvent('keydown', { metaKey: true });
|
||||
expect(isCtrlKeyPressed(event)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return true for ctrlKey press on non-macOS', () => {
|
||||
Object.defineProperty(navigator, 'userAgent', { value: 'windows' });
|
||||
const { isCtrlKeyPressed } = useDeviceSupport();
|
||||
const event = new KeyboardEvent('keydown', { ctrlKey: true });
|
||||
expect(isCtrlKeyPressed(event)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
44
packages/frontend/@n8n/composables/src/useDeviceSupport.ts
Normal file
44
packages/frontend/@n8n/composables/src/useDeviceSupport.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export function useDeviceSupport() {
|
||||
/**
|
||||
* Check if the device is a touch device but exclude devices that have a fine pointer (mouse or track-pad)
|
||||
* - `fine` will check for an accurate pointing device. Examples include mice, touch-pads, and drawing styluses
|
||||
* - `coarse` will check for a pointing device of limited accuracy. Examples include touchscreens and motion-detection sensors
|
||||
* - `any-pointer` will check for the presence of any pointing device, if there are multiple of them
|
||||
*/
|
||||
const isTouchDevice = ref(
|
||||
window.matchMedia('(any-pointer: coarse)').matches &&
|
||||
!window.matchMedia('(any-pointer: fine)').matches,
|
||||
);
|
||||
const userAgent = ref(navigator.userAgent.toLowerCase());
|
||||
|
||||
const isIOs = ref(
|
||||
userAgent.value.includes('iphone') ||
|
||||
userAgent.value.includes('ipad') ||
|
||||
userAgent.value.includes('ipod'),
|
||||
);
|
||||
const isAndroidOs = ref(userAgent.value.includes('android'));
|
||||
const isMacOs = ref(userAgent.value.includes('macintosh') || isIOs.value);
|
||||
const isMobileDevice = ref(isIOs.value || isAndroidOs.value);
|
||||
|
||||
const controlKeyCode = ref(isMacOs.value ? 'Meta' : 'Control');
|
||||
|
||||
function isCtrlKeyPressed(e: MouseEvent | KeyboardEvent): boolean {
|
||||
if (isMacOs.value) {
|
||||
return (e as KeyboardEvent).metaKey;
|
||||
}
|
||||
return (e as KeyboardEvent).ctrlKey;
|
||||
}
|
||||
|
||||
return {
|
||||
userAgent: userAgent.value,
|
||||
isTouchDevice: isTouchDevice.value,
|
||||
isAndroidOs: isAndroidOs.value,
|
||||
isIOs: isIOs.value,
|
||||
isMacOs: isMacOs.value,
|
||||
isMobileDevice: isMobileDevice.value,
|
||||
controlKeyCode: controlKeyCode.value,
|
||||
isCtrlKeyPressed,
|
||||
};
|
||||
}
|
||||
10
packages/frontend/@n8n/composables/tsconfig.json
Normal file
10
packages/frontend/@n8n/composables/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@n8n/frontend-typescript-config",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"types": ["vite/client", "vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts", "tsup.config.ts"]
|
||||
}
|
||||
11
packages/frontend/@n8n/composables/tsup.config.ts
Normal file
11
packages/frontend/@n8n/composables/tsup.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/useDeviceSupport.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
clean: true,
|
||||
dts: true,
|
||||
cjsInterop: true,
|
||||
splitting: true,
|
||||
sourcemap: true,
|
||||
});
|
||||
4
packages/frontend/@n8n/composables/vite.config.ts
Normal file
4
packages/frontend/@n8n/composables/vite.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { defineConfig, mergeConfig } from 'vite';
|
||||
import { vitestConfig } from '@n8n/frontend-vitest-config';
|
||||
|
||||
export default mergeConfig(defineConfig({}), vitestConfig);
|
||||
14
packages/frontend/tooling/eslint-config/index.cjs
Normal file
14
packages/frontend/tooling/eslint-config/index.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {import('@types/eslint').ESLint.ConfigData} config
|
||||
* @returns {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports.createFrontendEslintConfig = function (path, config = {}) {
|
||||
return {
|
||||
extends: ['@n8n_io/eslint-config/frontend'],
|
||||
...sharedOptions(path, 'frontend'),
|
||||
...config,
|
||||
};
|
||||
};
|
||||
3
packages/frontend/tooling/eslint-config/index.d.ts
vendored
Normal file
3
packages/frontend/tooling/eslint-config/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { ESLint } from '@types/eslint';
|
||||
|
||||
export function createFrontendEslintConfig(path: string): ESLint.ConfigData;
|
||||
23
packages/frontend/tooling/eslint-config/package.json
Normal file
23
packages/frontend/tooling/eslint-config/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@n8n/frontend-eslint-config",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@n8n_io/eslint-config": "workspace:*",
|
||||
"eslint-plugin-n8n-local-rules": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"index.cjs"
|
||||
],
|
||||
"main": "./index.cjs",
|
||||
"module": "./index.cjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.cjs",
|
||||
"require": "./index.cjs",
|
||||
"types": "./index.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"license": "See LICENSE.md file in the root of the repository"
|
||||
}
|
||||
18
packages/frontend/tooling/typescript-config/package.json
Normal file
18
packages/frontend/tooling/typescript-config/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@n8n/frontend-typescript-config",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"tsconfig.json"
|
||||
],
|
||||
"main": "./tsconfig.json",
|
||||
"module": "./tsconfig.json",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./tsconfig.json",
|
||||
"require": "./tsconfig.json"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"license": "See LICENSE.md file in the root of the repository"
|
||||
}
|
||||
13
packages/frontend/tooling/typescript-config/tsconfig.json
Normal file
13
packages/frontend/tooling/typescript-config/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"importHelpers": true,
|
||||
"allowJs": true,
|
||||
"incremental": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
3
packages/frontend/tooling/vitest-config/index.d.ts
vendored
Normal file
3
packages/frontend/tooling/vitest-config/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { UserConfig } from 'vitest/node';
|
||||
|
||||
export const vitestConfig: UserConfig;
|
||||
30
packages/frontend/tooling/vitest-config/index.mjs
Normal file
30
packages/frontend/tooling/vitest-config/index.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineConfig as defineVitestConfig } from 'vitest/config';
|
||||
|
||||
/**
|
||||
* Define a Vitest configuration
|
||||
*
|
||||
* @returns {import('vitest/node').UserConfig}
|
||||
*/
|
||||
export const vitestConfig = defineVitestConfig({
|
||||
test: {
|
||||
silent: true,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/__tests__/setup.ts'],
|
||||
...(process.env.COVERAGE_ENABLED === 'true'
|
||||
? {
|
||||
coverage: {
|
||||
enabled: true,
|
||||
provider: 'v8',
|
||||
reporter: process.env.CI === 'true' ? 'cobertura' : 'text-summary',
|
||||
all: true,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
css: {
|
||||
modules: {
|
||||
classNameStrategy: 'non-scoped',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
27
packages/frontend/tooling/vitest-config/package.json
Normal file
27
packages/frontend/tooling/vitest-config/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@n8n/frontend-vitest-config",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"peerDependencies": {
|
||||
"vite": "catalog:frontend",
|
||||
"vitest": "catalog:frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "catalog:frontend",
|
||||
"vitest": "catalog:frontend"
|
||||
},
|
||||
"files": [
|
||||
"index.mjs"
|
||||
],
|
||||
"main": "./index.mjs",
|
||||
"module": "./index.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.mjs",
|
||||
"require": "./index.mjs",
|
||||
"types": "./index.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"license": "See LICENSE.md file in the root of the repository"
|
||||
}
|
||||
Reference in New Issue
Block a user