diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.module.scss index bab4bd1135f0cf6259c4134306ba412cf331f750..5d3bd74fb573af0b9b6662c1c6d117dd86c864a2 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.module.scss +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.module.scss @@ -1,9 +1,11 @@ +@use './variables.module.scss'; + .attribute { display: flex; font-family: monospace; font-weight: bold; - font-size: 10px; - border-radius: 20px; + font-size: variables.$fontsize; + border-radius: 2px; } // .handle { @@ -19,11 +21,15 @@ // } .contentWrapper { - // margin-left: 2ch; display: flex; + align-items: center; .content { - padding: 4px 2ch; + padding: variables.$ypad 0 variables.$ypad 1ch; + max-width: 15ch; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } } @@ -36,10 +42,10 @@ input { background-color: lightgray; font-family: monospace; - font-size: 10px; + font-size: variables.$fontsize; border: 1px solid darkgrey; border-radius: 2px; - height: 10px; + height: variables.$height; outline: none; transition: border 0.3s; @@ -47,7 +53,4 @@ border: 1px solid grey; } } - // &:read-only { - // cursor: 'grab'; - // } } diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx index b45171f0c6cf753eb00a5750b8023429d923a813..2c98aacf45b980e4e771ed5caef3094aec0d82d4 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx @@ -1,8 +1,13 @@ +import { + CheckDatatypeConstraint, + GetAttributeBoolOperators, +} from '@graphpolaris/querybuilder/usecases'; import { useTheme } from '@mui/material'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Handle, Position } from 'react-flow-renderer'; import { Handles } from '../entitypill/entitypill'; import styles from './attributepill.module.scss'; +import AttributeOperatorSelect from './operatorselect'; /** * Component to render an attribute flow element @@ -13,38 +18,11 @@ export const AttributeRFPill = React.memo(({ data }: { data: any }) => { const [readonly, setReadonly] = useState(true); const [value, setValue] = useState(data?.value || ''); - /** Checks if the string input is a number. */ - const isNumber = (x: string): boolean => { - { - if (typeof x != 'string') return false; - return !Number.isNaN(x) && !Number.isNaN(parseFloat(x)); - } - }; - - /** Checks if the provided value is the same as the datatype of the attribute. */ - const inputConstraint = (type: string, str: string): string => { - let res = ''; - switch (type) { - case 'string': - res = str; - break; - case 'bool': - res = str; - break; // TODO: only false and true live update will break since it will not allow to write more that 1 letter - case 'int': - isNumber(str) ? (res = str) : (res = ''); - break; // TODO: check if letters after number - default: - res = str; - break; - } - return res; - }; - const onChange = (e: any) => { - if (data != undefined) { - setValue(inputConstraint(data.datatype, e.target.value)); - } + setValue(e.target.value); + }; + const validateInput = () => { + setValue(CheckDatatypeConstraint(data.datatype, value)); }; // Calculates the size of the input @@ -54,6 +32,11 @@ export const AttributeRFPill = React.memo(({ data }: { data: any }) => { return value.length; }; + const boolOperators = useMemo( + () => GetAttributeBoolOperators(data?.datatype), + [data?.datatype] + ); + return ( <div className={styles.attribute} @@ -69,18 +52,23 @@ export const AttributeRFPill = React.memo(({ data }: { data: any }) => { className={styles.handle} /> */} <div className={styles.contentWrapper}> - <span className={styles.content}>{data.name}</span> + <span className={styles.content} title={data.name}> + {data.name} + </span> + <AttributeOperatorSelect + selected={data?.operator} + options={boolOperators} + changed={(o) => console.log(o)} + /> <span className={styles.attributeInput}> <input style={{ maxWidth: `${getInputWidth()}ch` }} type="string" - // readOnly={readonly} placeholder={'?'} value={value} onChange={onChange} - // onDoubleClick={() => setReadonly(false)} - // onBlur={() => setReadonly(true)} - onKeyDown={(e) => e.key == 'Enter' && setReadonly(true)} + onBlur={validateInput} + onKeyDown={(e) => e.key == 'Enter' && validateInput()} ></input> </span> </div> diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..98afdc669ce8d4184c3cc990f965fa6d1d7f9a9c --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss @@ -0,0 +1,70 @@ +@use './variables.module.scss'; + +.container { + position: relative; + vertical-align: baseline; + margin: 0 1ch; + font-weight: normal; + font-size: 1.3em; +} + +.valueContainer { + color: #6a6a6a; + border: 1px solid rgba(0, 0, 0, 0); + border-radius: 2px; + background-color: transparent; + + transition: border-color 0.2s; + + height: variables.$height; + align-items: center; + display: flex; + padding: 0 1px; + + &.highlighted, + &:hover { + border-color: rgba(0, 0, 0, 0.4); + } +} + +.listbox { + font-size: 10px; + box-sizing: border-box; + padding: 5px; + margin: 5px 0 0 0; + list-style: none; + position: absolute; + height: auto; + box-shadow: 0 5px 13px -3px #e0e3e7; + background: white; + border: 1px solid #cdd2d7; + border-radius: 0.75em; + color: #1a2027; + overflow: auto; + z-index: 1; + outline: 0px; + left: -8px; + + &.hidden { + opacity: 0; + visibility: hidden; + transition: opacity 0.4s 0.1s ease, visibility 0.4s 0.1s step-end; + } + + & > li { + padding: 1px 4px; + border-radius: 2px; + + &.selected { + background: #f1f1f1; + } + + &:hover { + background: #e7ebf0; + } + + &[aria-selected='true'] { + background: #e0e3e7; + } + } +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f721cf54d56a8606d813bafb827d824b821a0ebb --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { SelectOption } from '@mui/base'; +import styles from './operatorselect.module.scss'; +import { useRef, useState } from 'react'; + +// const grey = { +// 100: '#E7EBF0', +// 200: '#E0E3E7', +// 300: '#CDD2D7', +// 400: '#B2BAC2', +// 500: '#A0AAB4', +// 600: '#6F7E8C', +// 700: '#3E5060', +// 800: '#2D3843', +// 900: '#1A2027', +// }; + +interface Props { + options: SelectOption<string>[]; + selected: string; + changed?: (newSelected: SelectOption<string>) => void; +} + +function AttributeOperatorSelect({ + options, + selected, + changed = () => {}, +}: Props) { + const listboxRef = useRef<HTMLUListElement>(null); + const [listboxVisible, setListboxVisible] = useState(false); + const [currSelected, setCurrSelected] = useState( + options.find((o) => o.value == selected)?.label || options[0].label + ); + + React.useEffect(() => { + if (listboxVisible) { + listboxRef.current?.focus(); + } + }, [listboxVisible]); + + const changeSelected = (option: SelectOption<string>) => { + if (option.label != currSelected) { + setCurrSelected(option.label); + changed(option); + } + }; + + return ( + <div + className={styles.container} + // onMouseOver={() => setListboxVisible(true)} + onMouseOut={() => setListboxVisible(false)} + onClick={() => setListboxVisible(true)} + onFocus={() => setListboxVisible(true)} + onBlur={() => setListboxVisible(false)} + > + <div + className={ + styles.valueContainer + ' ' + (listboxVisible && styles.highlighted) + } + > + {currSelected} + </div> + {options.length > 1 && ( + <ul + className={styles.listbox + ' ' + (!listboxVisible && styles.hidden)} + ref={listboxRef} + onMouseOver={() => setListboxVisible(true)} + > + {options.map((option) => ( + <li + className={option.label == currSelected ? styles.selected : ''} + key={option.value} + onClick={() => changeSelected(option)} + > + {option.label} + </li> + ))} + </ul> + )} + </div> + ); +} + +export default AttributeOperatorSelect; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/variables.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/variables.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..3ab1054c0e84797aca3206d659b69214decb60cf --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/variables.module.scss @@ -0,0 +1,3 @@ +$height: 5px; +$fontsize: 6px; +$ypad: 1.5px; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss index c649ef87390666179126faa50974460d8b168e54..97825709cfc561847a9db922f0ab960f38db9c3b 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss @@ -9,10 +9,12 @@ .contentWrapper { display: flex; + align-items: center; .handleLeft { position: relative; + top: 25%; border: 0px; border-radius: 0px; @@ -50,7 +52,7 @@ .handleRight { position: relative; - + top: 25%; border: 0px; border-radius: 0px; diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx index 492a00ffe59e4d38d1a02d6df553ce43c87d8fff..f6f98775d9efd9b66189e0599158e81d67f0e682 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx @@ -35,7 +35,19 @@ const initialElements = [ id: '2', type: 'attribute', position: { x: 180, y: 180 }, - data: { name: 'Attribute Pill', datatype: 'string' }, + data: { name: 'Attr Pill string', datatype: 'string', operator: 'EQ' }, + }, + { + id: '3', + type: 'attribute', + position: { x: 180, y: 210 }, + data: { name: 'Attr Pill number', datatype: 'float', operator: 'EQ' }, + }, + { + id: '4', + type: 'attribute', + position: { x: 180, y: 240 }, + data: { name: 'Attr Pill bool', datatype: 'bool', operator: 'EQ' }, }, ]; @@ -55,7 +67,7 @@ const QueryBuilder = (props: {}) => { elements={elements} style={graphStyles} snapGrid={[10, 10]} - snapToGrid={true} + // snapToGrid={true} nodeTypes={nodeTypes} > <Background gap={10} size={0.7} /> diff --git a/libs/querybuilder/usecases/.babelrc b/libs/querybuilder/usecases/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..cf7ddd99c615a064ac18eb3109eee4f394ab1faf --- /dev/null +++ b/libs/querybuilder/usecases/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] +} diff --git a/libs/querybuilder/usecases/.eslintrc.json b/libs/querybuilder/usecases/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..3456be9b9036a42c593c82b050281230e4ca0ae4 --- /dev/null +++ b/libs/querybuilder/usecases/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/querybuilder/usecases/README.md b/libs/querybuilder/usecases/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f919d5ec5c4710fe9263506b367251e7e6a92baa --- /dev/null +++ b/libs/querybuilder/usecases/README.md @@ -0,0 +1,7 @@ +# querybuilder-usecases + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test querybuilder-usecases` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/querybuilder/usecases/jest.config.js b/libs/querybuilder/usecases/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..941f4fbc2eed65bcc6b5a914c29364dcd1fff9bb --- /dev/null +++ b/libs/querybuilder/usecases/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'querybuilder-usecases', + preset: '../../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '<rootDir>/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/querybuilder/usecases', +}; diff --git a/libs/querybuilder/usecases/project.json b/libs/querybuilder/usecases/project.json new file mode 100644 index 0000000000000000000000000000000000000000..271a62582eacd5887d6adbf6a5027c3b12618eaf --- /dev/null +++ b/libs/querybuilder/usecases/project.json @@ -0,0 +1,23 @@ +{ + "root": "libs/querybuilder/usecases", + "sourceRoot": "libs/querybuilder/usecases/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/querybuilder/usecases/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/querybuilder/usecases"], + "options": { + "jestConfig": "libs/querybuilder/usecases/jest.config.js", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/querybuilder/usecases/src/index.ts b/libs/querybuilder/usecases/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4bef008f84019df612abc90c29fd3c17857368b --- /dev/null +++ b/libs/querybuilder/usecases/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/attribute/getAttributeBoolOperators'; +export * from './lib/attribute/checkInput'; diff --git a/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c8b0e625f274de9311eb612515c49452b154a75 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts @@ -0,0 +1,32 @@ +/** Checks if the string input is a number. */ +function isNumber(x: string): boolean { + if (typeof x != 'string') return false; + return !Number.isNaN(x) && !Number.isNaN(parseFloat(x)); +} +function isBoolean(s: string): boolean { + return s == 'true' || s == 'false' || s == '0' || s == '1'; +} +function toBoolean(s: string): string { + if (s == '1' || s == 'true') return 'true'; + return 'false'; +} + +/** Checks if the provided value is the same as the datatype of the attribute. */ +export function CheckDatatypeConstraint(type: string, str: string): string { + let res = ''; + switch (type) { + case 'string': + res = str; + break; + case 'bool': + isBoolean(str) ? (res = toBoolean(str)) : (res = ''); + break; + case 'int': + isNumber(str) ? (res = '' + parseFloat(str)) : (res = ''); + break; + default: + res = str; + break; + } + return res; +} diff --git a/libs/querybuilder/usecases/src/lib/attribute/getAttributeBoolOperators.ts b/libs/querybuilder/usecases/src/lib/attribute/getAttributeBoolOperators.ts new file mode 100644 index 0000000000000000000000000000000000000000..22809a7cde969f5be4da3978e650231a38ca1c4f --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/attribute/getAttributeBoolOperators.ts @@ -0,0 +1,63 @@ +/** Determines the available boolean operators for a certain datatype. */ +export function GetAttributeBoolOperators( + datatype: string +): { label: string; value: string }[] { + switch (datatype) { + case 'text': + case 'string': + return [ + { + label: '=', + value: 'EQ', + }, + { + label: '≠', + value: 'NEQ', + }, + { + label: 'inc', + value: 'includes', + }, + { + label: 'exc', + value: 'excludes', + }, + ]; + case 'int': + case 'float': + return [ + { + label: '=', + value: 'EQ', + }, + { + label: '≠', + value: 'NEQ', + }, + { + label: '>', + value: 'GT', + }, + { + label: '≥', + value: 'GTE', + }, + { + label: '<', + value: 'LT', + }, + { + label: '≤', + value: 'LTE', + }, + ]; + case 'bool': + default: + return [ + { + label: '=', + value: 'EQ', + }, + ]; + } +} diff --git a/libs/querybuilder/usecases/src/lib/querybuilder-usecases.spec.ts b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..684984e68f06f7cff30cc3f8c00749fd61c1aa9b --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.spec.ts @@ -0,0 +1,7 @@ +import { querybuilderUsecases } from './querybuilder-usecases'; + +describe('querybuilderUsecases', () => { + it('should work', () => { + expect(querybuilderUsecases()).toEqual('querybuilder-usecases'); + }); +}); diff --git a/libs/querybuilder/usecases/src/lib/querybuilder-usecases.ts b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.ts new file mode 100644 index 0000000000000000000000000000000000000000..06d687eb90fbdd6d6e752c90417a12a854aff52b --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.ts @@ -0,0 +1,3 @@ +export function querybuilderUsecases(): string { + return 'querybuilder-usecases'; +} diff --git a/libs/querybuilder/usecases/tsconfig.json b/libs/querybuilder/usecases/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..6ebadfb9de07f71cd20ee1102f3512550505ad2a --- /dev/null +++ b/libs/querybuilder/usecases/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true + } +} diff --git a/libs/querybuilder/usecases/tsconfig.lib.json b/libs/querybuilder/usecases/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..efdd77fbf5b34f06e8efa8ad8bc87e11a3c1e9af --- /dev/null +++ b/libs/querybuilder/usecases/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/querybuilder/usecases/tsconfig.spec.json b/libs/querybuilder/usecases/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..d8716fecfa3b7929f162b71e7a966c579a63c071 --- /dev/null +++ b/libs/querybuilder/usecases/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index cd9c7fc960effe1a638e83bd4e1dd03b2bcc53c0..0cda85af2f22f49c61c9982353183b8b67715dec 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,9 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@graphpolaris/querybuilder/usecases": [ + "libs/querybuilder/usecases/src/index.ts" + ], "@graphpolaris/schema-usecases": ["libs/schema/usecases/src/index.ts"], "@graphpolaris/shared/data-access/api": [ "libs/shared/data-access/api/src/index.ts" diff --git a/workspace.json b/workspace.json index 1e30cdac60853923072853d35375bf9d10055196..c25dfc2953a83e693380348b829d27bbb8b93e00 100644 --- a/workspace.json +++ b/workspace.json @@ -1,6 +1,7 @@ { "version": 2, "projects": { + "querybuilder-usecases": "libs/querybuilder/usecases", "schema-usecases": "libs/schema/usecases", "shared-data-access-api": "libs/shared/data-access/api", "shared-data-access-authorization": "libs/shared/data-access/authorization", @@ -10,4 +11,4 @@ "web-graphpolaris": "apps/web-graphpolaris", "web-graphpolaris-e2e": "apps/web-graphpolaris-e2e" } -} \ No newline at end of file +}