feat: Add User Management (#2636)

*  adjust tests

* 🛠 refactor user invites to be indempotent (#2791)

* 🔐 Encrypt SMTP pass for user management backend (#2793)

* 📦 Add crypto-js to /cli

* 📦 Update package-lock.json

*  Create type for SMTP config

*  Encrypt SMTP pass

*  Update format for `userManagement.emails.mode`

*  Update format for `binaryDataManager.mode`

*  Update format for `logs.level`

* 🔥 Remove logging

* 👕 Fix lint

* 👰  n8n 2826 um wedding FE<>BE (#2789)

* remove mocks

* update authorization func

* lock down default role

* 🐛 fix requiring authentication for OPTIONS requests

* 🐛 fix cors and cookie issues in dev

* update setup route

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

* update telemetry

* 🐛 preload role for users

* 🐛 remove auth for password reset routes

* 🐛 fix forgot-password flow

*  allow workflow tag disabling

* update telemetry init

* add reset

* clear error notifications on signin

* remove load settings from node view

* remove user id from user state

* inherit existing user props

* go back in history on button click

* use replace to force redirect

* update stories

*  add env check for tag create

* 🧪 Add `/users` tests for user management backend (#2790)

*  Refactor users namespace

*  Adjust fillout endpoint

*  Refactor initTestServer arg

* ✏️ Specify agent type

* ✏️ Specify role type

*  Tighten `/users/:id` check

*  Add initial tests

* 🚚 Reposition init server map

*  Set constants in `validatePassword()`

*  Tighten `/users/:id` check

*  Improve checks in `/users/:id`

*  Add tests for `/users/:id`

* 📦 Update package-lock.json

*  Simplify expectation

*  Reuse util for authless agent

* 🚚 Make role names consistent

* 📘 Tighten namespaces map type

* 🔥 Remove unneeded default arg

*  Add tests for `POST /users`

* 📘 Create test SMTP account type

* ✏️ Improve wording

* 🎨 Formatting

* 🔥 Remove temp fix

*  Replace helper with config call

*  Fix failing tests

* 🔥 Remove outdated test

* 🔥 Remove unused helper

*  Increase readability of domain fetcher

*  Refactor payload validation

* 🔥 Remove repetition

*  Restore logging

*  Initialize logger in tests

* 🔥 Remove redundancy from check

* 🚚 Move `globalOwnerRole` fetching to global scope

* 🔥 Remove unused imports

* 🚚 Move random utils to own module

* 🚚 Move test types to own module

* ✏️ Add dividers to utils

* ✏️ Reorder `initTestServer` param docstring

* ✏️ Add TODO comment

*  Dry up member creation

*  Tighten search criteria

* 🧪 Add expectation to `GET /users`

*  Create role fetcher utils

*  Create one more role fetch util

* 🔥 Remove unneeded DB query

* 🧪 Add expectation to `POST /users`

* 🧪 Add expectation to `DELETE /users/:id`

* 🧪 Add another expectation to `DELETE /users/:id`

* 🧪 Add expectations to `DELETE /users/:id`

* 🧪 Adjust expectations in `POST /users/:id`

* 🧪 Add expectations to `DELETE /users/:id`

* 👕 Fix build

*  Update method

* 📘 Fix `userToDelete` type

*  Refactor `createAgent()`

*  Make role fetching global

*  Optimize roles fetching

*  Centralize member creation

*  Refactor truncation helper

* 🧪 Add teardown to `DELETE /users/:id`

* 🧪 Add DB expectations to users tests

* 🔥 Remove pass validation due to hash

* ✏️ Improve pass validation error message

*  Improve owner pass validation

*  Create logger initialization helper

*  Optimize helpers

*  Restructure `getAllRoles` helper

* 🧪 Add password reset flow tests for user management backend (#2807)

*  Refactor users namespace

*  Adjust fillout endpoint

*  Refactor initTestServer arg

* ✏️ Specify agent type

* ✏️ Specify role type

*  Tighten `/users/:id` check

*  Add initial tests

* 🚚 Reposition init server map

*  Set constants in `validatePassword()`

*  Tighten `/users/:id` check

*  Improve checks in `/users/:id`

*  Add tests for `/users/:id`

* 📦 Update package-lock.json

*  Simplify expectation

*  Reuse util for authless agent

* 🚚 Make role names consistent

* 📘 Tighten namespaces map type

* 🔥 Remove unneeded default arg

*  Add tests for `POST /users`

* 📘 Create test SMTP account type

* ✏️ Improve wording

* 🎨 Formatting

* 🔥 Remove temp fix

*  Replace helper with config call

*  Fix failing tests

* 🔥 Remove outdated test

*  Add tests for password reset flow

* ✏️ Fix test wording

*  Set password reset namespace

* 🔥 Remove unused helper

*  Increase readability of domain fetcher

*  Refactor payload validation

* 🔥 Remove repetition

*  Restore logging

*  Initialize logger in tests

* 🔥 Remove redundancy from check

* 🚚 Move `globalOwnerRole` fetching to global scope

* 🔥 Remove unused imports

* 🚚 Move random utils to own module

* 🚚 Move test types to own module

* ✏️ Add dividers to utils

* ✏️ Reorder `initTestServer` param docstring

* ✏️ Add TODO comment

*  Dry up member creation

*  Tighten search criteria

* 🧪 Add expectation to `GET /users`

*  Create role fetcher utils

*  Create one more role fetch util

* 🔥 Remove unneeded DB query

* 🧪 Add expectation to `POST /users`

* 🧪 Add expectation to `DELETE /users/:id`

* 🧪 Add another expectation to `DELETE /users/:id`

* 🧪 Add expectations to `DELETE /users/:id`

* 🧪 Adjust expectations in `POST /users/:id`

* 🧪 Add expectations to `DELETE /users/:id`

* 📘 Add namespace name to type

* 🚚 Adjust imports

*  Optimize `globalOwnerRole` fetching

* 🧪 Add expectations

* 👕 Fix build

* 👕 Fix build

*  Update method

*  Update method

* 🧪 Fix `POST /change-password` test

* 📘 Fix `userToDelete` type

*  Refactor `createAgent()`

*  Make role fetching global

*  Optimize roles fetching

*  Centralize member creation

*  Refactor truncation helper

* 🧪 Add teardown to `DELETE /users/:id`

* 🧪 Add DB expectations to users tests

*  Refactor as in users namespace

* 🧪 Add expectation to `POST /change-password`

* 🔥 Remove pass validation due to hash

* ✏️ Improve pass validation error message

*  Improve owner pass validation

*  Create logger initialization helper

*  Optimize helpers

*  Restructure `getAllRoles` helper

*  Update `truncate` calls

* 🐛 return 200 for non-existing user

*  fix tests for forgot-password and user creation

* Update packages/editor-ui/src/components/MainSidebar.vue

Co-authored-by: Ahsan Virani <ahsan.virani@gmail.com>

* Update packages/editor-ui/src/components/Telemetry.vue

Co-authored-by: Ahsan Virani <ahsan.virani@gmail.com>

* Update packages/editor-ui/src/plugins/telemetry/index.ts

Co-authored-by: Ahsan Virani <ahsan.virani@gmail.com>

* Update packages/editor-ui/src/plugins/telemetry/index.ts

Co-authored-by: Ahsan Virani <ahsan.virani@gmail.com>

* Update packages/editor-ui/src/plugins/telemetry/index.ts

Co-authored-by: Ahsan Virani <ahsan.virani@gmail.com>

* 🚚 Fix imports

*  reset password just if password exists

* Fix validation at `PATCH /workfows/:id` (#2819)

* 🐛 Validate entity only if workflow

* 👕 Fix build

* 🔨 refactor response from user creation

* 🐛 um email invite fix (#2833)

* update users invite

* fix notificaitons stacking on top of each other

* remove unnessary check

* fix type issues

* update structure

* fix types

* 🐘  database migrations UM + password reset expiration (#2710)

* Add table prefix and assign existing workflows and credentials to owner for sqlite

* Added user management migration to MySQL

* Fixed some missing table prefixes and removed unnecessary user id

* Created migration for postgres and applies minor fixes

* Fixed migration for sqlite by removing the unnecessary index and for mysql by removing unnecessary user data

* Added password reset token expiration

* Addressing comments made by Ben

* ️ add missing tablePrefix

*  fix tests + add tests for expiring pw-reset-token

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

*  treat skipped personalizationSurvey as not answered

* 🐛 removing active workflows when deleting user, 🐛 fix reinvite, 🐛 fix resolve-signup-token, 🐘 remove workflowname uniqueness

*  Add DB state check tests (#2841)

* 🔥 Remove unneeded import

* 🔥 Remove unneeded vars

* ✏️ Improve naming

* 🧪 Add expectations to `POST /owner`

* 🧪 Add expectations to `PATCH /me`

* 🧪 Add expectation to `PATCH /me/password`

* ✏️ Clarify when owner is owner shell

* 🧪 Add more expectations

*  Restore package-lock to parent branch state

* Add logging to user management endpoints v2 (#2836)

*  Initialize logger in tests

*  Add logs to mailer

*  Add logs to middleware

*  Add logs to me endpoints

*  Add logs to owner endpoints

*  Add logs to pass flow endpoints

*  Add logs to users endpoints

* 📘 Improve typings

*  Merge two logs into one

*  Adjust log type

*  Add password reset email log

* ✏️ Reword log message

*  Adjust log meta object

*  Add total to log

* ✏️ Add detail to log message

* ✏️ Reword log message

* ✏️ Reword log message

* 🐛 Make total users to set up accurate

* ✏️ Reword `Logger.debug()` messages

* ✏️ Phrasing change for consistency

* 🐛 Fix ID overridden in range query

* 🔨 small refactoring

* 🔐 add auth to push-connection

* 🛠   Create credentials namespace and add tests (#2831)

* 🧪 Fix failing test

* 📘 Improve `createAgent` signature

* 🚚 Fix `LoggerProxy` import

*  Create credentials endpoints namespace

* 🧪 Set up initial tests

*  Add validation to model

*  Adjust validation

* 🧪 Add test

* 🚚 Sort creds endpoints

* ✏️ Plan out pending tests

* 🧪 Add deletion tests

* 🧪 Add patch tests

* 🧪 Add get cred tests

* 🚚 Hoist import

* ✏️ Make test descriptions consistent

* ✏️ Adjust description

* 🧪 Add missing test

* ✏️ Make get descriptions consistent

*  Undo line break

*  Refactor to simplify `saveCredential`

* 🧪 Add non-owned tests for owner

* ✏️ Improve naming

* ✏️ Add clarifying comments

* 🚚 Improve imports

*  Initialize config file

* 🔥 Remove unneeded import

* 🚚 Rename dir

*  Adjust deletion call

*  Adjust error code

* ✏️ Touch up comment

*  Optimize fetching with `@RelationId`

* 🧪 Add expectations

*  Simplify mock calls

* 📘 Set deep readonly to object constants

* 🔥 Remove unused param and encryption key

*  Add more `@RelationId` calls in models

*  Restore

* 🐛 no auth for .svg

* 🛠 move auth cookie name to constant; 🐛 fix auth for push-connection

*  Add auth middleware tests (#2853)

*  Simplify existing suite

* 🧪 Validate that auth cookie exists

* ✏️ Move comment

* 🔥 Remove unneeded imports

* ✏️ Add clarifying comments

* ✏️ Document auth endpoints

* 🧪 Add middleware tests

* ✏️ Fix typos

Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>

* 🔥 Remove test description wrappers (#2874)

* 🔥 Remove /owner test wrappers

* 🔥 Remove auth middleware test wrappers

* 🔥 Remove auth endpoints test wrappers

* 🔥 Remove overlooked middleware wrappers

* 🔥 Remove me namespace test wrappers

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

*  Runtime checks for credentials load and execute workflows (#2697)

* Runtime checks for credentials load and execute workflows

* Fixed from reviewers

* Changed runtime validation for credentials to be on start instead of on demand

* Refactored validations to use user id instead of whole User instance

* Removed user entity from workflow project because it is no longer needed

* General fixes and improvements to runtime checks

* Remove query builder and improve styling

* Fix lint issues

*  remove personalizationAnswers when fetching all users

*  fix failing get all users test

*  check authorization routes also for authentication

* 🐛 fix defaults in reset command

* 🛠 refactorings from walkthrough (#2856)

*  Make `getTemplate` async

*  Remove query builder from `getCredentials`

*  Add save manual executions log message

*  Restore and hide migrations logs

*  Centralize ignore paths check

* 👕 Fix build

* 🚚 Rename `hasOwner` to `isInstanceOwnerSetUp`

*  Add `isSetUp` flag to `User`

*  Add `isSetUp` to FE interface

*  Adjust `isSetUp` checks on FE

* 👕 Fix build

*  Adjust `isPendingUser()` check

* 🚚 Shorten helper name

*  Refactor as `isPending` per feedback

* ✏️ Update log message

*  Broaden check

* 🔥 Remove unneeded relation

*  Refactor query

* 🔥 Re-remove logs from migrations

* 🛠 set up credentials router (#2882)

*  Refactor creds endpoints into router

* 🧪 Refactor creds tests to use router

* 🚚 Rename arg for consistency

* 🚚 Move `credentials.api.ts` outside /public

* 🚚 Rename constant for consistency

* 📘 Simplify types

* 🔥 Remove unneeded arg

* 🚚 Rename router to controller

*  Shorten endpoint

*  Update `initTestServer()` arg

*  Mutate response body in GET /credentials

* 🏎 improve performance of type cast for FE

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

* 🐛 remove GET /login from auth

* 🔀 merge master + FE update (#2905)

*  Add Templates (#2720)

* Templates Bugs / Fixed Various Bugs / Multiply Api Request, Carousel Gradient, Core Nodes Filters ...

* Updated MainSidebar Paddings

* N8N-Templates Bugfixing - Remove Unnecesairy Icon (Shape), Refatctor infiniteScrollEnabled Prop + updated infiniterScroll functinality

* N8N-2853 Fixed Carousel Arrows Bug after Cleaning the SearchBar

* fix telemetry init

* fix search tracking issues

* N8N-2853 Created FilterTemplateNode Constant Array, Filter PlayButton and WebhookRespond from Nodes, Added Box for showing more nodes inside TemplateList, Updated NewWorkflowButton to primary, Fixed Markdown issue with Code

* N8N-2853 Removed Placeholder if Workflows Or Collections are not found, Updated the Logic

* fix telemetry events

* clean up session id

* update user inserted event

* N8N-2853 Fixed Categories to Moving if the names are long

* Add todos

* Update Routes on loading

* fix spacing

* Update Border Color

* Update Border Readius

* fix filter fn

* fix constant, console error

* N8N-2853 PR Fixes, Refactoring, Removing unnecesairy code ..

* N8N-2853 PR Fixes - Editor-ui Fixes, Refactoring, Removing Dead Code ...

* N8N-2853 Refactor Card to LongCard

* clean up spacing, replace css var

* clean up spacing

* set categories as optional in node

* replace vars

* refactor store

* remove unnesssary import

* fix error

* fix templates view to start

* add to cache

* fix coll view data

* fix categories

* fix category event

* fix collections carousel

* fix initial load and search

* fix infinite load

* fix query param

* fix scrolling issues

* fix scroll to top

* fix search

* fix collections search

* fix navigation bug

* rename view

* update package lock

* rename workflow view

* rename coll view

* update routes

* add wrapper component

* set session id

* fix search tracking

* fix session tracking

* remove deleted mutation

* remove check for unsupported nodes

* refactor filters

* lazy load template

* clean up types

* refactor infinte scroll

* fix end of search

* Fix spacing

* fix coll loading

* fix types

* fix coll view list

* fix navigation

* rename types

* rename state

* fix search responsiveness

* fix coll view spacing

* fix search view spacing

* clean up views

* set background color

* center page not vert

* fix workflow view

* remove import

* fix background color

* fix background

* clean props

* clean up imports

* refactor button

* update background color

* fix spacing issue

* rename event

* update telemetry event

* update endpoints, add loading view, check for endpoint health

* remove conolse log

* N8N-2853 Fixed Menu Items Padding

* replace endpoints

* fix type issues

* fix categories

* N8N-2853 Fixed ParameterInput Placeholder after ElementUI Upgrade

* update createdAt

*  Fix placeholder in creds config modal

* ✏️ Adjust docstring to `credText` placeholder version

* N8N-2853 Optimized

* N8N-2853 Optimized code

*  Add deployment type to FE settings

*  Add deployment type to interfaces

* N8N-2853 Removed Animated prop from components

*  Add deployment type to store module

*  Create hiring banner

*  Display hiring banner

*  Undo unrelated change

* N8N-2853 Refactor TemplateFilters

*  Fix indentation

* N8N-2853 Reorder items / TemplateList

* 👕 Fix lint

* N8N-2853 Refactor TemplateFilters Component

* N8N-2853 Reorder TemplateList

* refactor template card

* update timeout

* fix removelistener

* fix spacing

* split enabled from offline

* add spacing to go back

* N8N-2853 Fixed Screens for Tablet & Mobile

* N8N-2853 Update Stores Order

* remove image componet

* remove placeholder changes

* N8N-2853 Fixed Chinnese Placeholders for El Select Component that comes from the Library Upgrade

* N8N-2853 Fixed Vue Agile Console Warnings

* N8N-2853 Update Collection Route

* ✏️ Update jobs URL

* 🚚 Move logging to root component

*  Refactor `deploymentType` to `isInternalUser`

*  Improve syntax

* fix cut bug in readonly view

* N8N-3012 Fixed Details section in templates with lots of description, Fixed Mardown Block with overflox-x

* N8N-3012 Increased Font-size, Spacing and Line-height of the Categories Items

* N8N-3012 Fixed Vue-agile client width error on resize

* only delay redirect for root path

* N8N-3012 Fixed Carousel Arrows that Disappear

* N8N-3012 Make Loading Screen same color as Templates

* N8N-3012 Markdown renders inline block as block code

* add offline warning

* hide log from workflow iframe

* update text

* make search button larger

* N8N-3012 Categories / Tags extended all the way in details section

* load data in cred modals

* remove deleted message

* add external hook

* remove import

* update env variable description

* fix markdown width issue

* disable telemetry for demo, add session id to template pages

* fix telemetery bugs

* N8N-3012 Not found Collections/Wokrkflow

* N8N-3012 Checkboxes change order when categories are changed

* N8N-3012 Refactor SortedCategories inside TemplateFilters component

* fix firefox bug

* add telemetry requirements

* add error check

* N8N-3012 Update GoBackButton to check if Route History is present

* N8N-3012 Fixed WF Nodes Icons

* hide workflow screenshots

* remove unnessary mixins

* rename prop

* fix design a bit

* rename data

* clear workspace on destroy

* fix copy paste bug

* fix disabled state

* N8N-3012 Fixed Saving/Leave without saving Modal

* fix telemetry issue

* fix telemetry issues, error bug

* fix error notification

* disable workflow menu items on templates

* fix i18n elementui issue

* Remove Emit - NodeType from HoverableNodeIcon component

* TechnicalFixes: NavigateTo passed down as function should be helper

* TechnicalFixes: Update NavigateTo function

* TechnicalFixes: Add FilterCoreNodes directly as function

* check for empty connecitions

* fix titles

* respect new lines

* increase categories to be sliced

* rename prop

* onUseWorkflow

* refactor click event

* fix bug, refactor

* fix loading story

* add default

* fix styles at right level of abstraction

* add wrapper with width

* remove loading blocks component

* add story

* rename prop

* fix spacing

* refactor tag, add story

* move margin to container

* fix tag redirect, remove unnessary check

* make version optional

* rename view

* move from workflows to templates store

* remove unnessary change

* remove unnessary css

* rename component

* refactor collection card

* add boolean to prevent shrink

* clean up carousel

* fix redirection bug on save

* remove listeners to fix multiple listeners bug

* remove unnessary types

* clean up boolean set

* fix node select bug

* rename component

* remove unnessary class

* fix redirection bug

* remove unnessary error

* fix typo

* fix blockquotes, pre

* refactor markdown rendering

* remove console log

* escape markdown

* fix safari bug

* load active workflows to fix modal bug

* ⬆️ Update package-lock.json file

*  Add n8n version as header

Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>

* 🔖 Release n8n-workflow@0.88.0

* ⬆️ Set n8n-workflow@0.88.0 on n8n-core

* 🔖 Release n8n-core@0.106.0

* ⬆️ Set n8n-core@0.106.0 and n8n-workflow@0.88.0 on n8n-node-dev

* 🔖 Release n8n-node-dev@0.45.0

* ⬆️ Set n8n-core@0.106.0 and n8n-workflow@0.88.0 on n8n-nodes-base

* 🔖 Release n8n-nodes-base@0.163.0

* 🔖 Release n8n-design-system@0.12.0

* ⬆️ Set n8n-design-system@0.12.0 and n8n-workflow@0.88.0 on n8n-editor-ui

* 🔖 Release n8n-editor-ui@0.132.0

* ⬆️ Set n8n-core@0.106.0, n8n-editor-ui@0.132.0, n8n-nodes-base@0.163.0 and n8n-workflow@0.88.0 on n8n

* 🔖 Release n8n@0.165.0

* fix default user bug

* fix bug

* update package lock

* fix duplicate import

* fix settings

* fix templates access

Co-authored-by: Oliver Trajceski <olivertrajceski@yahoo.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>

*  n8n 2952 personalisation (#2911)

* refactor/update survey

* update customers

* Fix up personalization survey

* fix recommendation logic

* set to false

* hide suggested nodes when empty

* use keys

* add missing logic

* switch types

* Fix logic

* remove unused constants

* add back constant

* refactor filtering inputs

* hide last input on personal

* fix other

*  add current pw check for change password (#2912)

* fix back button

* Add current password input

* add to modal

* update package.json

* delete mock file

* delete mock file

* get settings func

* update router

* update package lock

* update package lock

* Fix invite text

* update error i18n

* open personalization on search if not set

* update error view i18n

* update change password

* update settings sidebar

* remove import

* fix sidebar

* 🥅 fix error for credential/workflow not found

* update invite modal

*  persist skipping owner setup (#2894)

* 🚧 added skipInstanceOwnerSetup to DB + route to save skipping

*  skipping owner setup persists

*  add tests for authorization and /owner/skip-setup

* 🛠 refactor FE settings getter

* 🛠 move setting setup stop to owner creation

* 🐛 fix wrong setting of User.isPending

* 🐛 fix isPending

* 🏷 add isPending to PublicUser

* 🐛 fix unused import

* update delete modal

* change password modal

* remove _label

* sort keys

* remove key

* update key names

* fix test endpoint

* 🥅 Handle error workflows permissions (#2908)

* Handle error workflows permissions

* Fixed wrong query format

* 🛠 refactor query

Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>

* fix ts issue

* fix list after ispending changes

* fix error page bugs

* fix error redirect

* fix notification

* 🐛 fix survey import in migration

* fix up spacing

* update keys spacing

* update keys

* add space

* update key

* fix up more spacing

* 🔐 add current password (#2919)

* add curr pass

* update key names

* 🐛 stringify tag ids

* 🔐 check current password before update

* add package lock

* fix dep version

* update version

* 🐛 fix access for instance owner to credentials (#2927)

* 🛠 stringify tag id on entity

* 🔐 Update password requirements (#2920)

*  Update password requirements

*  Adjust random helpers

*  fix tests for currentPassword check

* change redirection, add homepage

* fix error view redirection

* updated wording

* fix setup redirection

* update validator

* remove successfully

* update consumers

* update settings redirect

* on signup, redirect to homepage

* update empty state

* add space to emails

* remove brackets

* add opacity

* update spacing

* remove border from last user

* personal details updated

* update redirect on sign up

* prevent text wrap

* fix notification title line height

* remove console log

* 🐘 Support testing with Postgres and MySQL (#2886)

* 🗃️ Fix Postgres migrations

*  Add DB-specific scripts

*  Set up test connections

*  Add Postgres UUID check

* 🧪 Make test adjustments for Postgres

*  Refactor connection logic

*  Set up double init for Postgres

* ✏️ Add TODOs

*  Refactor DB dropping logic

*  Implement global teardown

*  Create TypeORM wrappers

*  Initial MySQL setup

*  Clean up Postgres connection options

*  Simplify by sharing bootstrap connection name

* 🗃️ Fix MySQL migrations

* 🔥 Remove comments

*  Use ES6 imports

* 🔥 Remove outdated comments

*  Centralize bootstrap connection name handles

*  Centralize database types

* ✏️ Update comment

* 🚚 Rename `findRepository`

* 🚧 Attempt to truncate MySQL

*  Implement creds router

* 🐛 Fix duplicated MySQL bootstrap

* 🐛 Fix misresolved merge conflict

* 🗃️ Fix tags migration

* 🗃️ Fix MySQL UM migration

* 🐛 Fix MySQL parallelization issues

* 📘 Augment TypeORM to prevent error

* 🔥 Remove comments

*  Support one sqlite DB per suite run

* 🚚 Move `testDb` to own module

* 🔥 Deduplicate bootstrap Postgres logic

* 🔥 Remove unneeded comment

*  Make logger init calls consistent

* ✏️ Improve comment

* ✏️ Add dividers

* 🎨 Improve formatting

* 🔥 Remove duplicate MySQL global setting

* 🚚 Move comment

*  Update default test script

* 🔥 Remove unneeded helper

*  Unmarshal answers from Postgres

* 🐛 Phase out `isTestRun`

*  Refactor `isEmailSetup`

* 🔥 Remove unneeded imports

*  Handle bootstrap connection errors

* 🔥 Remove unneeded imports

* 🔥 Remove outdated comments

* ✏️ Fix typos

* 🚚 Relocate `answersFormatter`

*  Undo package.json miscommit

* 🔥 Remove unneeded import

*  Refactor test DB prefixing

*  Add no-leftover check to MySQL

* 📦 Update package.json

*  Autoincrement on simulated MySQL truncation

* 🔥 Remove debugging queries

* ✏️ fix email template link expiry

* 🔥 remove unused import

*  fix testing email not sent error

* fix duplicate import

* add package lock

* fix export

* change opacity

* fix text issue

* update action box

* update error title

* update forgot password

* update survey

* update product text

* remove unset fields

* add category to page events

* remove duplicate import

* update key

* update key

* update label type

* 🎨 um/fe review (#2946)

* 🐳 Update Node.js versions of Docker images to 16

* 🐛 Fix that some keyboard shortcuts did no longer work

* N8N-3057 Fixed Keyboard shortcuts no longer working on / Fixed callDebounced function

* N8N-3057 Update Debounce Function

* N8N-3057 Refactor callDebounce function

* N8N-3057 Update Dobounce Function

* 🐛 Fix issue with tooltips getting displayed behind node details view

* fix tooltips z-index

* move all element ui components

* update package lock

* 🐛 Fix credentials list load issue (#2931)

* always fetch credentials

* only fetch credentials once

*  Allow to disable hiring banner (#2902)

*  Add flag

*  Adjust interfaces

*  Adjust store module

*  Adjust frontend settings

*  Adjust frontend display

* 🐛 Fix issue that ctrl + o did behave wrong on workflow templates page (#2934)

* N8N-3094 Workflow Templates cmd-o acts on the Preview/Iframe

* N8N-3094 Workflow Templates cmd-o acts on the Preview/Iframe

* disable shortcuts for preview

Co-authored-by: Mutasem <mutdmour@gmail.com>

* ⬆️ Update package-lock.json file

* 🐛 Fix sorting by field in Baserow Node (#2942)

This fixes a bug which currently leads to the "Sorting" option of the node to be ignored.

* 🐛 Fix some i18n line break issues

*  Add Odoo Node (#2601)

* added odoo scaffolding

* update getting data from odoo instance

* added scaffolding for main loop and request functions

* added functions for CRUD opperations

* improoved error handling for odooJSONRPCRequest

* updated odoo node and fixing nodelinter issues

* fixed alpabetical order

* fixed types in odoo node

* fixing linter errors

* fixing linter errors

* fixed data shape returned from man loop

* updated node input types, added fields list to models

* update when custom resource is selected options for fields list will be populated dynamicly

* minor fixes

* 🔨 fixed credential test, updating CRUD methods

* 🔨 added additional fields to crm resource

* 🔨 added descriptions, fixed credentials test bug

* 🔨 standardize node and descriptions design

* 🔨 removed comments

* 🔨 added pagination to getAll operation

*  removed leftover function from previous implementation, removed required from optional fields

*  fixed id field, added indication of type and if required to field description, replaced string input in filters to fetched list of fields

* 🔨 fetching list of models from odoo, added selection of fields to be returned to predefined models, fixes accordingly to review

*  Small improvements

* 🔨 extracted adress fields into collection, changed fields to include in descriptions, minor tweaks

*  Improvements

* 🔨 working on review

* 🔨 fixed linter errors

* 🔨 review wip

* 🔨 review wip

* 🔨 review wip

*  updated display name for URL in credentials

* 🔨 added checks for valid id to delete and update

*  Minor improvements

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>

* 🐛 Handle Wise SCA requests (#2734)

*  Improve Wise error message after previous change

* fix duplicate import

* add package lock

* fix export

* change opacity

* fix text issue

* update action box

* update error title

* update forgot password

* update survey

* update product text

* remove unset fields

* add category to page events

* remove duplicate import

* update key

* update key

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
Co-authored-by: Oliver Trajceski <olivertrajceski@yahoo.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Tom <19203795+that-one-tom@users.noreply.github.com>
Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: pemontto <939704+pemontto@users.noreply.github.com>

* Move owner skip from settings

* 🐛 SMTP fixes (#2937)

* 🔥 Remove `UM_` from SMTP env vars

* 🔥 Remove SMTP host default value

*  Update sender value

*  Update invite template

*  Update password reset template

*  Update `N8N_EMAIL_MODE` default value

* 🔥 Remove `EMAIL` from all SMTP vars

*  Implement `verifyConnection()`

* 🚚 Reposition comment

* ✏️ Fix typo

* ✏️ Minor env var documentation improvements

* 🎨 Fix spacing

* 🎨 Fix spacing

* 🗃️ Remove SMTP settings cache

*  Adjust log message

*  Update error message

* ✏️ Fix template typo

* ✏️ Adjust wording

*  Interpolate email into success toast

* ✏️ Adjust base message in `verifyConnection()`

*  Verify connection on password reset

*  Bring up POST /users SMTP check

* 🐛 remove cookie if cookie is not valid

*  verify connection on instantiation

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

* 🔊 create logger helper for migrations (#2944)

* 🔥 remove unused database

* 🔊 add migration logging for sqlite

* 🔥 remove unnecessary index creation

* ️ change log level to warn

* 🐛 Fix issue with workflow process to initialize db connection correctly (#2948)

* ✏️ update error messages for webhhook run/activation

* 📈 Implement telemetry events (#2868)

* Implement basic telemetry events

* Fixing user id as part of the telemetry data

* Added user id to be part of the tracked data

*  Create telemetry mock

* 🧪 Fix tests with telemetry mock

* 🧪 Fix missing key in authless endpoint

* 📘 Create authless request type

* 🔥 Remove log

* 🐛 Fix `migration_strategy` assignment

* 📘 Remove `instance_id` from `ITelemetryUserDeletionData`

*  Simplify concatenation

*  Simplify `track()` call signature

* Fixed payload of telemetry to always include user_id

* Fixing minor issues

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* 🔊 Added logs to credentials, executions and workflows (#2915)

* Added logs to credentials, executions and workflows

* Some updates according to ivov's feedback

*  update log levels

*  fix tests

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

* 🐛 fix telemetry error

* fix conflicts with master

* fix duplicate

* add package-lock

* 🐛 Um/fixes (#2952)

* add initials to avatar

* redirect to signin if invalid token

* update pluralization

* add auth page category

* data transferred

* touch up setup page

* update button to add cursor

* fix personalization modal not closing

* ✏️ fix environment name

* 🐛 fix disabling UM

* 🐛 fix email setup flag

* 🐛 FE fixes 1 (#2953)

* add initials to avatar

* redirect to signin if invalid token

* update pluralization

* add auth page category

* data transferred

* touch up setup page

* update button to add cursor

* fix personalization modal not closing

* capitalize labels, refactor text

* Fixed the issue with telemetry data missing for personalization survey

* Changed invite email text

* 🐛 Fix quotes issue with postgres migration (#2958)

* Changed text for invite link

* 🐛 fix reset command for mysql

*  fix race condition in test DB creation

* 🔐 block user creation if UM is disabled

* 🥅 improve smtp setup issue error

*  update error message

* refactor route rules

* set package lock

* fix access

* remove capitalize

* update input labels

* refactor heading

* change span to fragment

* add route types

* refactor views

*  fix increase timeout for mysql

*  correct logic of error message

* refactor view names

*  update randomString

* 📈 Added missing event regarding failed emails (#2964)

* replace label with info

* 🛠 refactor JWT-secret creation

* remove duplicate key

* remove unused part

* remove semicolon

* fix up i18n pattern

* update translation keys

* update urls

* support i18n in nds

* fix how external keys are handled

* add source

* 💥 update timestamp of UM migration

* ✏️ small message updates

* fix tracking

* update notification line-height

* fix avatar opacity

* fix up empty state

* shift focus to input

* 🔐 Disable basic auth after owner has been set up (#2973)

* Disable basic auth after owner has been set up

* Remove unnecessary comparison

* rename modal title

* 🐛 use pgcrypto extension for uuid creation (#2977)

* 📧 Added public url variable for emails (#2967)

* Added public url variable for emails

* Fixed base url for reset password - the current implementation overrides possibly existing path

* Change variable name to editorUrl

* Using correct name editorUrl for emails

* Changed variable description

* Improved base url naming and appending path so it remains consistent

* Removed trailing slash from editor base url

* 🌐 fix i18n pattern (#2970)

* fix up i18n pattern

* update translation keys

* update urls

* support i18n in nds

* fix how external keys are handled

* add source

* Um/fixes 1000 (#2980)

* fix select issue

* 😫 hacky solution to circumvent pgcrypto (#2979)

* fix owner bug after transfer. always fetch latest credentials

* add confirmation modal to setup

* Use webhook url as fallback when editor url is not defined

* fix enter bug

* update modal

* update modal

* update modal text, fix bug in settings view

* Updating editor url to not append path

* rename keys

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: Ahsan Virani <ahsan.virani@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: Oliver Trajceski <olivertrajceski@yahoo.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
Co-authored-by: Tom <19203795+that-one-tom@users.noreply.github.com>
Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: pemontto <939704+pemontto@users.noreply.github.com>
This commit is contained in:
Ben Hesseldieck
2022-03-14 14:46:32 +01:00
committed by GitHub
parent 761720621e
commit 7264239b83
262 changed files with 17737 additions and 3294 deletions

View File

@@ -0,0 +1,33 @@
import N8nActionBox from './ActionBox.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Atoms/ActionBox',
component: N8nActionBox,
argTypes: {
},
parameters: {
backgrounds: { default: '--color-background-light' },
},
};
const methods = {
onClick: action('click'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nActionBox,
},
template: '<n8n-action-box v-bind="$props" @click="onClick" />',
methods,
});
export const ActionBox = Template.bind({});
ActionBox.args = {
emoji: "😿",
heading: "Headline you need to know",
description: "Long description that you should know something is the way it is because of how it is. ",
buttonText: "Do something",
};

View File

@@ -0,0 +1,66 @@
<template functional>
<div :class="$style.container">
<div :class="$style.heading">
<component :is="$options.components.N8nHeading" size="xlarge" align="center">{{ props.heading }}</component>
</div>
<div :class="$style.description">
<n8n-text color="text-base"><span v-html="props.description"></span></n8n-text>
</div>
<component :is="$options.components.N8nButton" :label="props.buttonText" size="large"
@click="(e) => listeners.click && listeners.click(e)"
/>
</div>
</template>
<script lang="ts">
import N8nButton from '../N8nButton';
import N8nHeading from '../N8nHeading';
import N8nText from '../N8nText';
export default {
name: 'n8n-action-box',
props: {
heading: {
type: String,
},
buttonText: {
type: String,
},
description: {
type: String,
},
},
components: {
N8nButton,
N8nHeading,
N8nText,
},
};
</script>
<style lang="scss" module>
.container {
border: 2px dashed var(--color-foreground-base);
border-radius: var(--border-radius-large);
display: flex;
flex-direction: column;
align-items: center;
padding: var(--spacing-3xl) 20%;
> * {
margin-bottom: var(--spacing-l);
}
}
.emoji {
font-size: 40px;
}
.heading {
margin-bottom: var(--spacing-l);
}
.description {
margin-bottom: var(--spacing-xl);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nActionBox from './ActionBox.vue';
export default N8nActionBox;

View File

@@ -0,0 +1,43 @@
import N8nActionToggle from './ActionToggle.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Atoms/ActionToggle',
component: N8nActionToggle,
argTypes: {
placement: {
type: 'select',
options: ['top', 'bottom'],
},
},
parameters: {
backgrounds: { default: '--color-background-light' },
},
};
const methods = {
onAction: action('action'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nActionToggle,
},
template: '<div style="height:300px;width:300px;display:flex;align-items:center;justify-content:center"><n8n-action-toggle v-bind="$props" @action="onAction" /></div>',
methods,
});
export const ActionToggle = Template.bind({});
ActionToggle.args = {
actions: [
{
label: 'Go',
value: 'go',
},
{
label: 'Stop',
value: 'stop',
},
],
};

View File

@@ -0,0 +1,71 @@
<template>
<span :class="$style.container">
<el-dropdown :placement="placement" trigger="click" @command="onCommand">
<span :class="$style.button">
<component :is="$options.components.N8nIcon"
icon="ellipsis-v"
/>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="action in actions"
:key="action.value"
:command="action.value"
>
{{action.label}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
</template>
<script lang="ts">
import ElDropdown from 'element-ui/lib/dropdown';
import ElDropdownMenu from 'element-ui/lib/dropdown-menu';
import ElDropdownItem from 'element-ui/lib/dropdown-item';
import N8nIcon from '../N8nIcon';
export default {
name: 'n8n-action-toggle',
components: {
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
N8nIcon,
},
props: {
actions: {
type: Array,
default: () => [],
},
placement: {
type: String,
default: 'bottom',
validator: (value: string): boolean =>
['top', 'bottom'].includes(value),
},
},
methods: {
onCommand(value: string) {
this.$emit('action', value) ;
},
},
};
</script>
<style lang="scss" module>
.container > * {
line-height: 1;
}
.button {
cursor: pointer;
padding: var(--spacing-4xs);
border-radius: var(--border-radius-base);
color: var(--color-text-dark);
&:focus {
background-color: var(--color-background-xlight);
}
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nActionToggle from './ActionToggle.vue';
export default N8nActionToggle;

View File

@@ -0,0 +1,25 @@
import N8nAvatar from './Avatar.vue';
export default {
title: 'Atoms/Avatar',
component: N8nAvatar,
argTypes: {
size: {
type: 'select',
options: ['small', 'medium', 'large'],
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nAvatar,
},
template: '<n8n-avatar v-bind="$props" />',
});
export const Avatar = Template.bind({});
Avatar.args = {
name: 'Sunny Side',
};

View File

@@ -0,0 +1,89 @@
<template functional>
<span :class="$style.container">
<component
v-if="props.firstName"
:is="$options.components.Avatar"
:size="$options.methods.getSize(props.size)"
:name="props.firstName + ' ' + props.lastName"
variant="marble"
:colors="$options.methods.getColors(props.colors)"
/>
<div
v-else
:class="$style.empty"
:style="$options.methods.getBlankStyles(props.size)">
</div>
<span v-if="props.firstName" :class="$style.initials">{{$options.methods.getInitials(props)}}</span>
</span>
</template>
<script lang="ts">
import Avatar from 'vue2-boring-avatars';
const sizes: {[size: string]: number} = {
small: 28,
large: 48,
medium: 40,
};
export default {
name: 'n8n-avatar',
props: {
firstName: {
type: String,
},
lastName: {
type: String,
},
size: {
type: String,
default: 'medium',
},
colors: {
default: () => (['--color-primary', '--color-secondary', '--color-avatar-accent-1', '--color-avatar-accent-2', '--color-primary-tint-1']),
},
},
components: {
Avatar,
},
methods: {
getInitials({firstName, lastName}) {
return firstName.charAt(0) + lastName.charAt(0);
},
getBlankStyles(size): {height: string, width: string} {
const px = sizes[size];
return { height: `${px}px`, width: `${px}px` };
},
getColors(colors): string[] {
const style = getComputedStyle(document.body);
return colors.map((color: string) => style.getPropertyValue(color));
},
getSize(size: string): number {
return sizes[size];
},
},
};
</script>
<style lang="scss" module>
.container {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
}
.empty {
border-radius: 50%;
background-color: var(--color-foreground-dark);
opacity: .3;
}
.initials {
position: absolute;
font-size: var(--font-size-2xs);
font-weight: var(--font-weight-bold);
color: var(--color-text-xlight);
text-shadow: 0px 1px 6px rgba(25, 11, 9, 0.3);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nAvatar from './Avatar.vue';
export default N8nAvatar;

View File

@@ -0,0 +1,28 @@
import N8nBadge from './Badge.vue';
export default {
title: 'Atoms/Badge',
component: N8nBadge,
argTypes: {
theme: {
type: 'text',
options: ['default', 'secondary'],
},
size: {
type: 'select',
options: ['small', 'medium', 'large'],
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nBadge,
},
template:
'<n8n-badge v-bind="$props">Badge</n8n-badge>',
});
export const Badge = Template.bind({});
Badge.args = {};

View File

@@ -0,0 +1,58 @@
<template functional>
<span
:class="$style[props.theme]"
>
<component :is="$options.components.N8nText" :size="props.size" :bold="props.bold" :compact="true">
<slot></slot>
</component>
</span>
</template>
<script lang="ts">
import N8nText from '../N8nText';
export default {
props: {
theme: {
type: String,
default: 'default',
validator: (value: string) => ['default', 'secondary'].includes(value),
},
size: {
type: String,
default: 'small',
},
bold: {
type: Boolean,
default: false,
},
},
components: {
N8nText,
},
};
</script>
<style lang="scss" module>
.badge {
display: inline-flex;
align-items: center;
padding: var(--spacing-5xs) var(--spacing-4xs);
border: var(--border-base);
white-space: nowrap;
}
.default {
composes: badge;
border-radius: var(--border-radius-base);
color: var(--color-text-light);
border-color: var(--color-text-light);
}
.secondary {
composes: badge;
border-radius: var(--border-radius-xlarge);
color: var(--color-secondary);
background-color: var(--color-secondary-tint-1);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nBadge from './Badge.vue';
export default N8nBadge;

View File

@@ -47,18 +47,18 @@ export default {
type: String,
default: 'primary',
validator: (value: string): boolean =>
['primary', 'outline', 'light', 'text'].indexOf(value) !== -1,
['primary', 'outline', 'light', 'text'].includes(value),
},
theme: {
type: String,
validator: (value: string): boolean =>
['success', 'warning', 'danger'].indexOf(value) !== -1,
['success', 'warning', 'danger'].includes(value),
},
size: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['mini', 'small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1,
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
},
loading: {
type: Boolean,
@@ -82,7 +82,7 @@ export default {
float: {
type: String,
validator: (value: string): boolean =>
['left', 'right'].indexOf(value) !== -1,
['left', 'right'].includes(value),
},
fullWidth: {
type: Boolean,
@@ -122,9 +122,7 @@ export default {
</script>
<style lang="scss" module>
@function lightness($h, $s, $l, $lightness) {
@return hsl(var(#{$h}), var(#{$s}), calc(var(#{$l}) + #{$lightness}));
}
@import "../../utils";
.button {
> i {

View File

@@ -0,0 +1,69 @@
import N8nFormBox from './FormBox.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Modules/FormBox',
component: N8nFormBox,
argTypes: {
},
parameters: {
backgrounds: { default: '--color-background-light' },
},
};
const methods = {
onSubmit: action('submit'),
onInput: action('input'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nFormBox,
},
template: '<n8n-form-box v-bind="$props" @submit="onSubmit" @input="onInput" />',
methods,
});
export const FormBox = Template.bind({});
FormBox.args = {
title: 'Form title',
inputs: [
{
name: 'email',
properties: {
label: 'Your Email',
type: 'email',
required: true,
validationRules: [{name: 'VALID_EMAIL'}],
},
},
{
name: 'message',
properties: {
label: 'Please contact someone someday.',
type: 'text',
},
},
{
name: 'password',
properties: {
label: 'Your Password',
type: 'password',
required: true,
validationRules: [{name: 'DEFAULT_PASSWORD_RULES'}],
},
},
{
name: 'nickname',
properties: {
label: 'Your Nickname',
placeholder: 'Monty',
},
},
],
buttonText: 'Action',
redirectText: 'Go somewhere',
redirectLink: 'https://n8n.io',
};

View File

@@ -0,0 +1,160 @@
<template>
<div
:class="$style.container"
>
<div
v-if="title"
:class="$style.heading"
>
<n8n-heading
size="xlarge"
>
{{title}}
</n8n-heading>
</div>
<div
:class="$style.inputsContainer"
>
<n8n-form-inputs
:inputs="inputs"
:eventBus="formBus"
:columnView="true"
@input="onInput"
@submit="onSubmit"
/>
</div>
<div :class="$style.buttonsContainer" v-if="secondaryButtonText || buttonText">
<span
v-if="secondaryButtonText"
:class="$style.secondaryButtonContainer"
>
<n8n-link
size="medium"
theme="text"
@click="onSecondaryButtonClick"
>
{{secondaryButtonText}}
</n8n-link>
</span>
<n8n-button
v-if="buttonText"
:label="buttonText"
:loading="buttonLoading"
size="large"
@click="onButtonClick"
/>
</div>
<div :class="$style.actionContainer">
<n8n-link
v-if="redirectText && redirectLink"
:to="redirectLink"
>
{{redirectText}}
</n8n-link>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import N8nFormInputs from '../N8nFormInputs';
import N8nHeading from '../N8nHeading';
import N8nLink from '../N8nLink';
import N8nButton from '../N8nButton';
export default Vue.extend({
name: 'n8n-form-box',
components: {
N8nHeading,
N8nFormInputs,
N8nLink,
N8nButton,
},
props: {
title: {
type: String,
},
inputs: {
type: Array,
default() {
return [];
},
},
buttonText: {
type: String,
},
buttonLoading: {
type: Boolean,
default: false,
},
secondaryButtonText: {
type: String,
},
redirectText: {
type: String,
},
redirectLink: {
type: String,
},
},
data() {
return {
formBus: new Vue(),
};
},
methods: {
onInput(e: {name: string, value: string}) {
this.$emit('input', e);
},
onSubmit(e: {[key: string]: string}) {
this.$emit('submit', e);
},
onButtonClick() {
this.formBus.$emit('submit');
},
onSecondaryButtonClick(e) {
this.$emit('secondaryClick', e);
},
},
});
</script>
<style lang="scss" module>
.heading {
display: flex;
justify-content: center;
margin-bottom: var(--spacing-xl);
}
.container {
background-color: var(--color-background-xlight);
padding: var(--spacing-l);
border: var(--border-base);
border-radius: var(--border-radius-large);
box-shadow: 0px 4px 16px rgba(99, 77, 255, 0.06);
}
.inputsContainer {
margin-bottom: var(--spacing-xl);
}
.actionContainer {
display: flex;
justify-content: center;
}
.buttonsContainer {
composes: actionContainer;
margin-bottom: var(--spacing-s);
}
.secondaryButtonContainer {
flex-grow: 1;
display: flex;
align-items: center;
}
.withLabel {
margin-bottom: var(--spacing-s);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nFormBox from './FormBox.vue';
export default N8nFormBox;

View File

@@ -0,0 +1,35 @@
import N8nFormInput from './FormInput.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Modules/FormInput',
component: N8nFormInput,
argTypes: {
},
};
const methods = {
onInput: action('input'),
onFocus: action('focus'),
onChange: action('change'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nFormInput,
},
template: '<n8n-form-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" />',
methods,
data() {
return {
val: '',
};
},
});
export const FormInput = Template.bind({});
FormInput.args = {
label: 'Label',
placeholder: 'placeholder',
};

View File

@@ -0,0 +1,253 @@
<template>
<n8n-input-label :label="label" :tooltipText="tooltipText" :required="required && showRequiredAsterisk">
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter">
<slot v-if="hasDefaultSlot"></slot>
<n8n-select
v-else-if="type === 'select' || type === 'multi-select'"
:value="value"
:placeholder="placeholder"
:multiple="type === 'multi-select'"
@change="onInput"
@focus="onFocus"
@blur="onBlur"
ref="input"
>
<n8n-option
v-for="option in (options || [])"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</n8n-select>
<n8n-input
v-else
:type="type"
:placeholder="placeholder"
:value="value"
:maxlength="maxlength"
:autocomplete="autocomplete"
@input="onInput"
@blur="onBlur"
@focus="onFocus"
ref="input"
/>
</div>
<div :class="$style.errors" v-if="showErrors">
<span>{{ validationError }}</span>
<n8n-link
v-if="documentationUrl && documentationText"
:to="documentationUrl"
:newWindow="true"
size="small"
theme="danger"
>
{{ documentationText }}
</n8n-link>
</div>
<div :class="$style.infoText" v-else-if="infoText">
<span size="small">{{ infoText }}</span>
</div>
</n8n-input-label>
</template>
<script lang="ts">
import Vue from 'vue';
import N8nInput from '../N8nInput';
import N8nSelect from '../N8nSelect';
import N8nOption from '../N8nOption';
import N8nInputLabel from '../N8nInputLabel';
import { getValidationError, VALIDATORS } from './validators';
import { Rule, RuleGroup, IValidator } from "../../../../editor-ui/src/Interface";
import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins';
export default mixins(Locale).extend({
name: 'n8n-form-input',
components: {
N8nInput,
N8nInputLabel,
N8nOption,
N8nSelect,
},
data() {
return {
hasBlurred: false,
isTyping: false,
};
},
props: {
value: {
},
label: {
type: String,
},
infoText: {
type: String,
},
required: {
type: Boolean,
},
showRequiredAsterisk: {
type: Boolean,
default: true,
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
},
tooltipText: {
type: String,
},
showValidationWarnings: {
type: Boolean,
},
validateOnBlur: {
type: Boolean,
default: true,
},
documentationUrl: {
type: String,
},
documentationText: {
type: String,
default: 'Open docs',
},
validationRules: {
type: Array,
},
validators: {
type: Object,
},
maxlength: {
type: Number,
},
options: {
type: Array,
},
autocomplete: {
type: String,
},
focusInitially: {
type: Boolean,
},
},
mounted() {
this.$emit('validate', !this.validationError);
if (this.focusInitially && this.$refs.input) {
this.$refs.input.focus();
}
},
computed: {
validationError(): string | null {
const error = this.getValidationError();
if (error) {
return this.t(error.messageKey, error.options);
}
return null;
},
hasDefaultSlot(): boolean {
return !!this.$slots.default;
},
showErrors(): boolean {
return (
!!this.validationError &&
((this.validateOnBlur && this.hasBlurred && !this.isTyping) || this.showValidationWarnings)
);
},
},
methods: {
getValidationError(): ReturnType<IValidator['validate']> {
const rules = (this.validationRules || []) as (Rule | RuleGroup)[];
const validators = {
...VALIDATORS,
...(this.validators || {}),
} as { [key: string]: IValidator | RuleGroup };
if (this.required) {
const error = getValidationError(this.value, validators, validators.REQUIRED as Validator);
if (error) {
return error;
}
}
for (let i = 0; i < rules.length; i++) {
if (rules[i].hasOwnProperty('name')) {
const rule = rules[i] as Rule;
if (validators[rule.name]) {
const error = getValidationError(
this.value,
validators,
validators[rule.name] as Validator,
rule.config,
);
if (error) {
return error;
}
}
}
if (rules[i].hasOwnProperty('rules')) {
const rule = rules[i] as RuleGroup;
const error = getValidationError(
this.value,
validators,
rule,
);
if (error) {
return error;
}
}
}
return null;
},
onBlur() {
this.hasBlurred = true;
this.isTyping = false;
this.$emit('blur');
},
onInput(value: any) {
this.isTyping = true;
this.$emit('input', value);
},
onFocus() {
this.$emit('focus');
},
onEnter(e) {
e.stopPropagation();
e.preventDefault();
this.$emit('enter');
},
},
watch: {
validationError(newValue: string | null, oldValue: string | null) {
this.$emit('validate', !newValue);
},
},
});
</script>
<style lang="scss" module>
.infoText {
margin-top: var(--spacing-2xs);
font-size: var(--font-size-2xs);
font-weight: var(--font-weight-regular);
color: var(--color-text-base);
}
.errors {
composes: infoText;
color: var(--color-danger);
}
.errorInput {
--input-border-color: var(--color-danger);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nFormInput from './FormInput.vue';
export default N8nFormInput;

View File

@@ -0,0 +1,158 @@
import { IValidator, RuleGroup } from "../../../../editor-ui/src/Interface";
export const emailRegex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
REQUIRED: {
validate: (value: string | number | boolean | null | undefined) => {
if (typeof value === 'string' && !!value.trim()) {
return false;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return false;
}
return {
messageKey: 'formInput.validator.fieldRequired',
};
},
},
MIN_LENGTH: {
validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => {
if (typeof value === 'string' && value.length < config.minimum) {
return {
messageKey: 'formInput.validator.minCharactersRequired',
options: config,
};
}
return false;
},
},
MAX_LENGTH: {
validate: (value: string | number | boolean | null | undefined, config: { maximum: number }) => {
if (typeof value === 'string' && value.length > config.maximum) {
return {
messageKey: 'formInput.validator.maxCharactersRequired',
options: config,
};
}
return false;
},
},
CONTAINS_NUMBER: {
validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => {
if (typeof value !== 'string') {
return false;
}
const numberCount = (value.match(/\d/g) || []).length;
if (numberCount < config.minimum) {
return {
messageKey: 'formInput.validator.numbersRequired',
options: config,
};
}
return false;
},
},
VALID_EMAIL: {
validate: (value: string | number | boolean | null | undefined) => {
if (!emailRegex.test(String(value).trim().toLowerCase())) {
return {
messageKey: 'formInput.validator.validEmailRequired',
};
}
return false;
},
},
CONTAINS_UPPERCASE: {
validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => {
if (typeof value !== 'string') {
return false;
}
const uppercaseCount = (value.match(/[A-Z]/g) || []).length;
if (uppercaseCount < config.minimum) {
return {
messageKey: 'formInput.validator.uppercaseCharsRequired',
options: config,
};
}
return false;
},
},
DEFAULT_PASSWORD_RULES: {
rules: [
{
rules: [
{ name: 'MIN_LENGTH', config: { minimum: 8 } },
{ name: 'CONTAINS_NUMBER', config: { minimum: 1 } },
{ name: 'CONTAINS_UPPERCASE', config: { minimum: 1 } },
],
defaultError: {
messageKey: 'formInput.validator.defaultPasswordRequirements',
},
},
{ name: 'MAX_LENGTH', config: {maximum: 64} },
],
},
};
export const getValidationError = (
value: any, // tslint:disable-line:no-any
validators: { [key: string]: IValidator | RuleGroup },
validator: IValidator | RuleGroup,
config?: any, // tslint:disable-line:no-any
): ReturnType<IValidator['validate']> => {
if (validator.hasOwnProperty('rules')) {
const rules = (validator as RuleGroup).rules;
for (let i = 0; i < rules.length; i++) {
if (rules[i].hasOwnProperty('rules')) {
const error = getValidationError(
value,
validators,
rules[i] as RuleGroup,
config,
);
if (error) {
return error;
}
}
if (rules[i].hasOwnProperty('name') ) {
const rule = rules[i] as {name: string, config?: any}; // tslint:disable-line:no-any
if (!validators[rule.name]) {
continue;
}
const error = getValidationError(
value,
validators,
validators[rule.name] as IValidator,
rule.config,
);
if (error && (validator as RuleGroup).defaultError !== undefined) {
// @ts-ignore
return validator.defaultError;
} else if (error) {
return error;
}
}
}
} else if (
validator.hasOwnProperty('validate')
) {
return (validator as IValidator).validate(value, config);
}
return false;
};

View File

@@ -0,0 +1,73 @@
import N8nFormInputs from './FormInputs.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Modules/FormInputs',
component: N8nFormInputs,
argTypes: {},
parameters: {
backgrounds: { default: '--color-background-light' },
},
};
const methods = {
onInput: action('input'),
onSubmit: action('submit'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nFormInputs,
},
template: '<n8n-form-inputs v-bind="$props" @submit="onSubmit" @input="onInput" />',
methods,
});
export const FormInputs = Template.bind({});
FormInputs.args = {
inputs: [
{
name: 'email',
properties: {
label: 'Your Email',
type: 'email',
required: true,
initialValue: 'test@test.com',
},
},
{
name: 'password',
properties: {
label: 'Your Password',
type: 'password',
required: true,
},
},
{
name: 'nickname',
properties: {
label: 'Your Nickname',
placeholder: 'Monty',
},
},
{
name: 'opts',
properties: {
type: 'select',
label: 'Opts',
options: [
{
label: 'Opt1',
value: 'opt1',
},
{
label: 'Opt2',
value: 'opt2',
},
],
},
},
],
};

View File

@@ -0,0 +1,131 @@
<template>
<ResizeObserver
:breakpoints="[{bp: 'md', width: 500}]"
>
<template v-slot="{ bp }">
<div :class="bp === 'md' || columnView? $style.grid : $style.gridMulti">
<div
v-for="(input) in filteredInputs"
:key="input.name"
>
<n8n-text color="text-base" v-if="input.properties.type === 'info'" tag="div" align="center">
{{input.properties.label}}
</n8n-text>
<n8n-form-input
v-else
v-bind="input.properties"
:value="values[input.name]"
:showValidationWarnings="showValidationWarnings"
@input="(value) => onInput(input.name, value)"
@validate="(value) => onValidate(input.name, value)"
@enter="onSubmit"
/>
</div>
</div>
</template>
</ResizeObserver>
</template>
<script lang="ts">
import Vue from 'vue';
import N8nFormInput from '../N8nFormInput';
import { IFormInputs } from '../../Interface';
import ResizeObserver from '../ResizeObserver';
export default Vue.extend({
name: 'n8n-form-inputs',
components: {
N8nFormInput,
ResizeObserver,
},
props: {
inputs: {
type: Array,
default() {
return [[]];
},
},
eventBus: {
type: Vue,
},
columnView: {
type: Boolean,
},
},
data() {
return {
showValidationWarnings: false,
values: {} as {[key: string]: any},
validity: {} as {[key: string]: boolean},
};
},
mounted() {
(this.inputs as IFormInputs).forEach((input: IFormInput) => {
if (input.hasOwnProperty('initialValue')) {
Vue.set(this.values, input.name, input.initialValue);
}
});
if (this.eventBus) {
this.eventBus.$on('submit', this.onSubmit);
}
},
computed: {
filteredInputs(): IFormInput[] {
return this.inputs.filter((input: IFormInput) => typeof input.shouldDisplay === 'function'? input.shouldDisplay(this.values): true);
},
isReadyToSubmit(): boolean {
for (let key in this.validity) {
if (!this.validity[key]) {
return false;
}
}
return true;
},
},
methods: {
onInput(name: string, value: any) {
this.values = {
...this.values,
[name]: value,
};
this.$emit('input', {name, value});
},
onValidate(name: string, valid: boolean) {
Vue.set(this.validity, name, valid);
},
onSubmit() {
this.showValidationWarnings = true;
if (this.isReadyToSubmit) {
const toSubmit = this.filteredInputs.reduce((accu, input: IFormInput) => {
if (this.values[input.name]) {
accu[input.name] = this.values[input.name];
}
return accu;
}, {});
this.$emit('submit', toSubmit);
}
},
},
watch: {
isReadyToSubmit(ready: boolean) {
this.$emit('ready', ready);
},
},
});
</script>
<style lang="scss" module>
.grid {
display: grid;
grid-row-gap: var(--spacing-s);
grid-column-gap: var(--spacing-2xs);
}
.gridMulti {
composes: grid;
grid-template-columns: repeat(2, 1fr);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nFormInputs from './FormInputs.vue';
export default N8nFormInputs;

View File

@@ -1,5 +1,5 @@
<template functional>
<component :is="props.tag" :class="$style[$options.methods.getClass(props)]" :style="$options.methods.getStyles(props)">
<component :is="props.tag" :class="$options.methods.getClasses(props, $style)" :style="$options.methods.getStyles(props)">
<slot></slot>
</component>
</template>
@@ -25,16 +25,23 @@ export default {
type: String,
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'].includes(value),
},
align: {
type: String,
validator: (value: string): boolean => ['right', 'left', 'center'].includes(value),
},
},
methods: {
getClass(props: {size: string, bold: boolean}) {
return `heading-${props.size}${props.bold ? '-bold' : '-regular'}`;
getClasses(props: {size: string, bold: boolean}, $style: any) {
return {[$style[`size-${props.size}`]]: true, [$style.bold]: props.bold, [$style.regular]: !props.bold};
},
getStyles(props: {color: string}) {
const styles = {} as any;
if (props.color) {
styles.color = `var(--color-${props.color})`;
}
if (props.align) {
styles['text-align'] = props.align;
}
return styles;
},
},
@@ -50,79 +57,29 @@ export default {
font-weight: var(--font-weight-regular);
}
.heading-2xlarge {
.size-2xlarge {
font-size: var(--font-size-2xl);
line-height: var(--font-line-height-compact);
}
.heading-2xlarge-regular {
composes: regular;
composes: heading-2xlarge;
}
.heading-2xlarge-bold {
composes: bold;
composes: heading-2xlarge;
}
.heading-xlarge {
.size-xlarge {
font-size: var(--font-size-xl);
line-height: var(--font-line-height-compact);
}
.heading-xlarge-regular {
composes: regular;
composes: heading-xlarge;
}
.heading-xlarge-bold {
composes: bold;
composes: heading-xlarge;
}
.heading-large {
.size-large {
font-size: var(--font-size-l);
line-height: var(--font-line-height-loose);
}
.heading-large-regular {
composes: regular;
composes: heading-large;
}
.heading-large-bold {
composes: bold;
composes: heading-large;
}
.heading-medium {
.size-medium {
font-size: var(--font-size-m);
line-height: var(--font-line-height-loose);
}
.heading-medium-regular {
composes: regular;
composes: heading-medium;
}
.heading-medium-bold {
composes: bold;
composes: heading-medium;
}
.heading-small {
.size-small {
font-size: var(--font-size-s);
line-height: var(--font-line-height-regular);
}
.heading-small-regular {
composes: regular;
composes: heading-small;
}
.heading-small-bold {
composes: bold;
composes: heading-small;
}
</style>

View File

@@ -1,13 +0,0 @@
import { N8nComponent, N8nComponentSize } from '../component';
/** Button Component */
export declare class N8nIcon extends N8nComponent {
/** icon name, accepts an icon name of font awesome icon component */
icon: string;
/** Size of icon */
size: N8nComponentSize;
/** Whether icon should be spinning */
spin: boolean;
}

View File

@@ -1,3 +1,3 @@
import Icon from './Icon.vue';
import N8nIcon from './Icon.vue';
export default Icon;
export default N8nIcon;

View File

@@ -8,7 +8,7 @@ export default {
argTypes: {
type: {
control: 'select',
options: ['text', 'textarea'],
options: ['text', 'textarea', 'number', 'password', 'email'],
},
placeholder: {
control: 'text',
@@ -30,8 +30,8 @@ export default {
const methods = {
onInput: action('input'),
onFocus: action('input'),
onChange: action('input'),
onFocus: action('focus'),
onChange: action('change'),
};
const Template = (args, { argTypes }) => ({

View File

@@ -5,6 +5,7 @@
:size="$options.methods.getSize(props.size)"
:class="$style[$options.methods.getClass(props)]"
:ref="data.ref"
:autoComplete="props.autocomplete"
v-on="listeners"
>
<template v-slot:prepend>
@@ -36,13 +37,13 @@ export default {
type: {
type: String,
validator: (value: string): boolean =>
['text', 'textarea', 'number', 'password'].indexOf(value) !== -1,
['text', 'textarea', 'number', 'password', 'email'].includes(value),
},
size: {
type: String,
default: 'large',
validator: (value: string): boolean =>
['mini', 'small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1,
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
},
placeholder: {
type: String,
@@ -62,6 +63,10 @@ export default {
title: {
type: String,
},
autocomplete: {
type: String,
default: 'off',
},
},
methods: {
getSize(size: string): string | undefined {

View File

@@ -1,6 +1,6 @@
<template functional>
<div :class="{[$style.inputLabelContainer]: !props.labelHoverableOnly}">
<div :class="{[$style.inputLabel]: props.labelHoverableOnly, [$options.methods.getLabelClass(props, $style)]: true}">
<div :class="$options.methods.getLabelClass(props, $style)">
<component v-if="props.label" :is="$options.components.N8nText" :bold="props.bold" :size="props.size" :compact="!props.underline">
{{ props.label }}
<component :is="$options.components.N8nText" color="primary" :bold="props.bold" :size="props.size" v-if="props.required">*</component>
@@ -68,11 +68,19 @@ export default {
return '';
}
const classes = [];
if (props.underline) {
return $style[`label-${props.size}-underline`];
classes.push($style[`label-${props.size}-underline`]);
}
else {
classes.push($style[`label-${props.size}`]);
}
return $style[`label-${props.size}`];
if (props.labelHoverableOnly) {
classes.push($style.inputLabel);
}
return classes;
},
},
};

View File

@@ -0,0 +1,33 @@
import N8nLink from './Link.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Atoms/Link',
component: N8nLink,
argTypes: {
size: {
control: {
type: 'select',
options: ['small', 'medium', 'large'],
},
},
},
};
const methods = {
onClick: action('click'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nLink,
},
template: '<n8n-link v-bind="$props" @click="onClick">hello world</n8n-link>',
methods,
});
export const Link = Template.bind({});
Link.args = {
href: 'https://n8n.io/',
};

View File

@@ -0,0 +1,108 @@
<template functional>
<component :is="$options.components.N8nRoute" :to="props.to" :newWindow="props.newWindow"
@click="listeners.click"
>
<span
:class="$style[`${props.underline ? `${props.theme}-underline` : props.theme}`]"
>
<component :is="$options.components.N8nText" :size="props.size" :bold="props.bold">
<slot></slot>
</component>
</span>
</component>
</template>
<script lang="ts">
import Vue from 'vue';
import N8nText from '../N8nText';
import N8nRoute from '../N8nRoute';
export default {
name: 'n8n-link',
props: {
size: {
type: String,
},
to: {
type: String || Object,
},
newWindow: {
type: Boolean || undefined,
default: undefined,
},
bold: {
type: Boolean,
default: false,
},
underline: {
type: Boolean,
default: false,
},
theme: {
type: String,
default: 'primary',
validator: (value: string): boolean =>
['primary', 'danger', 'text'].includes(value),
},
},
components: {
N8nText,
N8nRoute,
},
};
</script>
<style lang="scss" module>
@import "../../utils";
.primary {
color: var(--color-primary);
&:active {
color: saturation(
--color-primary-h,
--color-primary-s,
--color-primary-l,
-(30%)
);
}
}
.text {
color: var(--color-text-base);
&:active {
color: saturation(
--color-primary-h,
--color-primary-s,
--color-primary-l,
-(30%)
);
}
}
.danger {
color: var(--color-danger);
&:active {
color: saturation(
--color-danger-h,
--color-danger-s,
--color-danger-l,
-(20%)
);
}
}
.primary-underline {
composes: primary;
text-decoration: underline;
}
.danger-underline {
composes: danger;
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nLink from './Link.vue';
export default N8nLink;

View File

@@ -3,8 +3,9 @@
:is="$options.components.ElMenu"
:defaultActive="props.defaultActive"
:collapse="props.collapse"
:router="props.router"
:class="$style[props.type + (props.light ? '-light' : '')]"
@select="listeners.select"
@select="(e) => listeners.select && listeners.select(e)"
>
<slot></slot>
</component>
@@ -30,6 +31,9 @@ export default {
light: {
type: Boolean,
},
router: {
type: Boolean,
},
},
components: {
ElMenu,
@@ -44,18 +48,23 @@ export default {
.primary {
composes: menu;
--menu-item-hover-font-color: var(--color-primary);
}
.secondary {
composes: menu;
--menu-font-color: var(--color-text-base);
--menu-item-font-color: var(--font-weight-regular);
--menu-item-font-weight: var(--font-weight-regular);
--menu-background-color: transparent;
--menu-item-hover-font-color: var(--color-primary);
--menu-item-active-font-color: var(--color-text-dark);
--menu-item-active-background-color: var(--color-foreground-base);
--menu-item-hover-font-color: var(--color-primary);
--menu-item-border-radius: 4px;
--menu-item-height: 38px;
li {
padding-left: 12px !important;
}
}
.secondary-light {

View File

@@ -0,0 +1,59 @@
<template functional>
<span>
<router-link
v-if="$options.methods.useRouterLink(props)"
:to="props.to"
@click="(e) => listeners.click && listeners.click(e)"
>
<slot></slot>
</router-link>
<a
v-else
:href="props.to"
@click="(e) => listeners.click && listeners.click(e)"
:target="$options.methods.openNewWindow(props) ? '_blank': '_self'"
>
<slot></slot>
</a>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
export default {
name: 'n8n-route',
props: {
to: {
type: String || Object,
},
newWindow: {
type: Boolean || undefined,
default: undefined,
},
},
methods: {
useRouterLink(props: {to: object | string, newWindow: boolean | undefined}) {
if (props.newWindow === true) {
// router-link does not support click events and opening in new window
return false;
}
if (typeof props.to === 'string') {
return props.to.startsWith('/');
}
return props.to !== undefined;
},
openNewWindow(props: {to: string, newWindow: boolean | undefined}) {
if (props.newWindow !== undefined) {
return props.newWindow;
}
if (typeof props.to === 'string') {
return !props.to.startsWith('/');
}
return true;
},
},
};
</script>

View File

@@ -0,0 +1,3 @@
import N8nRoute from './Route.vue';
export default N8nRoute;

View File

@@ -42,7 +42,7 @@ export default {
type: String,
default: 'large',
validator: (value: string): boolean =>
['mini', 'small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1,
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
},
placeholder: {
type: String,

View File

@@ -19,7 +19,7 @@ export default {
size: {
type: String,
validator: function (value: string): boolean {
return ['small', 'medium', 'large'].indexOf(value) !== -1;
return ['small', 'medium', 'large'].includes(value);
},
},
},

View File

@@ -1,7 +1,7 @@
<template functional>
<span :class="$style[$options.methods.getClass(props)]" :style="$options.methods.getStyles(props)">
<component :is="props.tag" :class="$options.methods.getClasses(props, $style)" :style="$options.methods.getStyles(props)">
<slot></slot>
</span>
</component>
</template>
<script lang="ts">
@@ -16,7 +16,7 @@ export default Vue.extend({
size: {
type: String,
default: 'medium',
validator: (value: string): boolean => ['xsmall', 'mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
validator: (value: string): boolean => ['xsmall', 'small', 'medium', 'large', 'xlarge'].includes(value),
},
color: {
type: String,
@@ -30,10 +30,14 @@ export default Vue.extend({
type: Boolean,
default: false,
},
tag: {
type: String,
default: 'span',
},
},
methods: {
getClass(props: {size: string, bold: boolean}) {
return `body-${props.size}${props.bold ? '-bold' : '-regular'}`;
getClasses(props: {size: string, bold: boolean}, $style: any) {
return {[$style[`size-${props.size}`]]: true, [$style.bold]: props.bold, [$style.regular]: !props.bold};
},
getStyles(props: {color: string, align: string, compact: false}) {
const styles = {} as any;
@@ -61,80 +65,29 @@ export default Vue.extend({
font-weight: var(--font-weight-regular);
}
.body-xlarge {
.size-xlarge {
font-size: var(--font-size-xl);
line-height: var(--font-line-height-xloose);
}
.body-xlarge-regular {
composes: regular;
composes: body-xlarge;
}
.body-xlarge-bold {
composes: bold;
composes: body-xlarge;
}
.body-large {
.size-large {
font-size: var(--font-size-m);
line-height: var(--font-line-height-xloose);
}
.body-large-regular {
composes: regular;
composes: body-large;
}
.body-large-bold {
composes: bold;
composes: body-large;
}
.body-medium {
.size-medium {
font-size: var(--font-size-s);
line-height: var(--font-line-height-loose);
}
.body-medium-regular {
composes: regular;
composes: body-medium;
}
.body-medium-bold {
composes: bold;
composes: body-medium;
}
.body-small {
.size-small {
font-size: var(--font-size-2xs);
line-height: var(--font-line-height-loose);
}
.body-small-regular {
composes: regular;
composes: body-small;
}
.body-small-bold {
composes: bold;
composes: body-small;
}
.body-xsmall {
.size-xsmall {
font-size: var(--font-size-3xs);
line-height: var(--font-line-height-compact);
}
.body-xsmall-regular {
composes: regular;
composes: body-xsmall;
}
.body-xsmall-bold {
composes: bold;
composes: body-xsmall;
}
</style>

View File

@@ -0,0 +1,39 @@
import N8nUserInfo from './UserInfo.vue';
export default {
title: 'Modules/UserInfo',
component: N8nUserInfo,
parameters: {
backgrounds: { default: '--color-background-light' },
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nUserInfo,
},
template:
'<n8n-user-info v-bind="$props" />',
});
export const Member = Template.bind({});
Member.args = {
firstName: 'Oscar',
lastName: 'Wilde',
email: 'test@n8n.io',
};
export const Current = Template.bind({});
Current.args = {
firstName: 'Ham',
lastName: 'Sam',
email: 'test@n8n.io',
isCurrentUser: true,
};
export const Invited = Template.bind({});
Invited.args = {
email: 'test@n8n.io',
isPendingUser: true,
};

View File

@@ -0,0 +1,90 @@
<template>
<div :class="$style.container">
<div :class="$style.avatarContainer">
<n8n-avatar :firstName="firstName" :lastName="lastName" />
</div>
<div v-if="isPendingUser" :class="$style.pendingUser">
<n8n-text :bold="true">{{email}}</n8n-text>
<span :class="$style.pendingBadge"><n8n-badge :bold="true">Pending</n8n-badge></span>
</div>
<div v-else :class="$style.infoContainer">
<div>
<n8n-text :bold="true">{{firstName}} {{lastName}} {{isCurrentUser ? this.t('nds.userInfo.you') : ''}}</n8n-text>
</div>
<div>
<n8n-text size="small" color="text-light">{{email}}</n8n-text>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import N8nText from '../N8nText';
import N8nAvatar from '../N8nAvatar';
import N8nBadge from '../N8nBadge';
import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins';
export default mixins(Locale).extend({
name: 'n8n-users-info',
components: {
N8nAvatar,
N8nText,
N8nBadge,
},
props: {
firstName: {
type: String,
},
lastName: {
type: String,
},
email: {
type: String,
},
isPendingUser: {
type: Boolean,
},
isCurrentUser: {
type: Boolean,
},
},
});
</script>
<style lang="scss" module>
.container {
display: inline-flex;
overflow: hidden;
}
.avatarContainer {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-text-light);
}
.infoContainer {
flex-grow: 1;
display: inline-flex;
flex-direction: column;;
justify-content: center;
margin-left: var(--spacing-xs);
}
.pendingUser {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: var(--spacing-xs);
}
.pendingBadge {
margin-left: var(--spacing-xs);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nUserInfo from './UserInfo.vue';
export default N8nUserInfo;

View File

@@ -0,0 +1,69 @@
import N8nUserSelect from './UserSelect.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Modules/UserSelect',
component: N8nUserSelect,
argTypes: {
},
parameters: {
backgrounds: { default: '--color-background-light' },
},
};
const methods = {
onChange: action('change'),
onBlur: action('blur'),
onFocus: action('focus'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nUserSelect,
},
template:
'<n8n-user-select v-bind="$props" v-model="val" @change="onChange" @blur="onBlur" @focus="onFocus" />',
methods,
data() {
return {
val: '',
};
},
});
export const UserSelect = Template.bind({});
UserSelect.args = {
users: [
{
id: "1",
firstName: 'Sunny',
lastName: 'Side',
email: "sunny@n8n.io",
globalRole: {
name: 'owner',
id: "1",
},
},
{
id: "2",
firstName: 'Kobi',
lastName: 'Dog',
email: "kobi@n8n.io",
globalRole: {
name: 'member',
id: "2",
},
},
{
id: "3",
email: "invited@n8n.io",
globalRole: {
name: 'member',
id: "2",
},
},
],
placeholder: 'Select user to transfer to',
currentUserId: "1",
};

View File

@@ -0,0 +1,145 @@
<template>
<el-select
:value="value"
:filterable="true"
:filterMethod="setFilter"
:placeholder="t('nds.userSelect.selectUser')"
:default-first-option="true"
:popper-append-to-body="true"
:popper-class="$style.limitPopperWidth"
:noDataText="t('nds.userSelect.noMatchingUsers')"
@change="onChange"
@blur="onBlur"
@focus="onFocus"
>
<el-option
v-for="user in sortedUsers"
:key="user.id"
:value="user.id"
:class="$style.itemContainer"
:label="getLabel(user)"
>
<n8n-user-info v-bind="user" :isCurrentUser="currentUserId === user.id" />
</el-option>
</el-select>
</template>
<script lang="ts">
import Vue from 'vue';
import N8nUserInfo from '../N8nUserInfo';
import { IUser } from '../../Interface';
import ElSelect from 'element-ui/lib/select';
import ElOption from 'element-ui/lib/option';
import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins';
export default mixins(Locale).extend({
name: 'n8n-user-select',
components: {
N8nUserInfo,
ElSelect,
ElOption,
},
props: {
users: {
type: Array,
default() {
return [];
},
},
value: {
type: String,
default: '',
},
ignoreIds: {
type: Array,
default() {
return [];
},
validator: (ids: string[]) => !ids.find((id) => typeof id !== 'string'),
},
currentUserId: {
type: String,
},
},
data() {
return {
filter: '',
};
},
computed: {
fitleredUsers(): IUser[] {
return this.users
.filter((user: IUser) => {
if (user.isPendingUser || !user.email) {
return false;
}
if (this.ignoreIds && this.ignoreIds.includes(user.id)) {
return false;
}
if (user.fullName) {
const match = user.fullName.toLowerCase().includes(this.filter.toLowerCase());
if (match) {
return true;
}
}
return user.email.includes(this.filter);
});
},
sortedUsers(): IUser[] {
return [...(this.fitleredUsers as IUser[])].sort((a: IUser, b: IUser) => {
if (a.lastName && b.lastName && a.lastName !== b.lastName) {
return a.lastName > b.lastName ? 1 : -1;
}
if (a.firstName && b.firstName && a.firstName !== b.firstName) {
return a.firstName > b.firstName? 1 : -1;
}
return a.email > b.email ? 1 : -1;
});
},
},
methods: {
setFilter(value: string) {
this.filter = value;
},
onChange(value: string) {
this.$emit('input', value);
},
onBlur() {
this.$emit('blur');
},
onFocus() {
this.$emit('focus');
},
getLabel(user: IUser) {
if (!user.fullName) {
return user.email;
}
return `${user.fullName} (${user.email})`;
},
},
});
</script>
<style lang="scss" module>
.itemContainer {
--select-option-padding: var(--spacing-2xs) var(--spacing-s);
--select-option-line-height: 1;
}
.limitPopperWidth {
width: 0;
li > span {
text-overflow: ellipsis;
overflow-x: hidden;
}
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nUserSelect from './UserSelect.vue';
export default N8nUserSelect;

View File

@@ -0,0 +1,73 @@
import N8nUsersList from './UsersList.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Modules/UsersList',
component: N8nUsersList,
argTypes: {
},
parameters: {
backgrounds: { default: '--color-background-light' },
},
};
const methods = {
onReinvite: action('reinvite'),
onDelete: action('delete'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nUsersList,
},
template:
'<n8n-users-list v-bind="$props" @reinvite="onReinvite" @delete="onDelete" />',
methods,
});
export const UsersList = Template.bind({});
UsersList.args = {
users: [
{
id: "1",
firstName: 'Sunny',
lastName: 'Side',
fullName: 'Sunny Side',
email: "sunny@n8n.io",
isDefaultUser: false,
isPendingUser: false,
isOwner: true,
globalRole: {
name: 'owner',
id: 1,
},
},
{
id: "2",
firstName: 'Kobi',
lastName: 'Dog',
fullName: 'Kobi Dog',
email: "kobi@n8n.io",
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
globalRole: {
name: 'member',
id: "2",
},
},
{
id: "3",
email: "invited@n8n.io",
isDefaultUser: false,
isPendingUser: true,
isOwner: false,
globalRole: {
name: 'member',
id: "2",
},
},
],
currentUserId: "1",
};

View File

@@ -0,0 +1,149 @@
<template>
<div>
<div
v-for="(user, i) in sortedUsers"
:key="user.id"
:class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder"
>
<n8n-user-info v-bind="user" :isCurrentUser="currentUserId === user.id" />
<div :class="$style.badgeContainer">
<n8n-badge v-if="user.isOwner" theme="secondary">{{ t('nds.auth.roles.owner') }}</n8n-badge>
<n8n-action-toggle
v-if="!user.isOwner"
placement="bottom"
:actions="getActions(user)"
@action="(action) => onUserAction(user, action)"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { IUser } from '../../Interface';
import Vue from 'vue';
import N8nActionToggle from '../N8nActionToggle';
import N8nBadge from '../N8nBadge';
import N8nIcon from '../N8nIcon';
import N8nLink from '../N8nLink';
import N8nText from '../N8nText';
import N8nUserInfo from '../N8nUserInfo';
import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins';
export default mixins(Locale).extend({
name: 'n8n-users-list',
components: {
N8nActionToggle,
N8nBadge,
N8nUserInfo,
},
props: {
users: {
type: Array,
required: true,
default() {
return [];
},
},
currentUserId: {
type: String,
},
},
computed: {
sortedUsers(): IUser[] {
return [...(this.users as IUser[])].sort((a: IUser, b: IUser) => {
// invited users sorted by email
if (a.isPendingUser && b.isPendingUser) {
return a.email > b.email ? 1 : -1;
}
if (a.isPendingUser) {
return -1;
}
if (b.isPendingUser) {
return 1;
}
if (a.isOwner) {
return -1;
}
if (b.isOwner) {
return 1;
}
if (a.lastName && b.lastName && a.firstName && b.firstName) {
if (a.lastName !== b.lastName) {
return a.lastName > b.lastName ? 1 : -1;
}
if (a.firstName !== b.firstName) {
return a.firstName > b.firstName? 1 : -1;
}
}
return a.email > b.email ? 1 : -1;
});
},
},
methods: {
getActions(user: IUser) {
const DELETE = {
label: this.t('nds.usersList.deleteUser'),
value: 'delete',
};
const REINVITE = {
label: this.t('nds.usersList.reinviteUser'),
value: 'reinvite',
};
if (user.isOwner) {
return [];
}
if (user.firstName) {
return [
DELETE,
];
}
else {
return [
REINVITE,
DELETE,
];
}
},
onUserAction(user: IUser, action: string) {
if (action === 'delete' || action === 'reinvite') {
this.$emit(action, user.id);
}
},
},
});
</script>
<style lang="scss" module>
.itemContainer {
display: flex;
padding: var(--spacing-2xs) 0 vaR(--spacing-2xs) 0;
> *:first-child {
flex-grow: 1;
}
}
.itemWithBorder {
composes: itemContainer;
border-bottom: var(--border-base);
}
.badgeContainer {
display: flex;
align-items: center;
> * {
margin-left: var(--spacing-2xs);
}
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nUsersList from './UsersList.vue';
export default N8nUsersList;

View File

@@ -0,0 +1,69 @@
<template>
<div ref="root">
<slot :bp="bp"></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'ResizeObserver',
props: {
enabled: {
type: Boolean,
default: true,
},
breakpoints: {
type: Array,
validator: (bps: Array<{bp: string, width: number}>) => {
return Array.isArray(bps) && bps.reduce(
(accu, {width, bp}) => accu && typeof width === 'number' && typeof bp === 'string'
, true);
},
},
},
data(): {observer: ResizeObserver | null, width: number | null} {
return {
observer: null,
bp: '',
};
},
mounted() {
if (!this.$props.enabled) {
return;
}
const bps = [...(this.breakpoints || [])].sort((a, b) => a.width - b.width);
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
// We wrap it in requestAnimationFrame to avoid this error - ResizeObserver loop limit exceeded
requestAnimationFrame(() => {
const newWidth = entry.contentRect.width;
let newBP = 'default';
for (let i = 0; i < bps.length; i++) {
if (newWidth < bps[i].width) {
newBP = bps[i].bp;
break;
}
}
this.bp = newBP;
});
});
});
this.$data.observer = observer;
if (this.$refs.root) {
observer.observe(this.$refs.root);
}
},
beforeDestroy() {
if (this.$props.enabled) {
this.$data.observer.disconnect();
}
},
});
</script>

View File

@@ -0,0 +1,3 @@
import ResizeObserver from './ResizeObserver.vue';
export default ResizeObserver;

View File

@@ -31,7 +31,15 @@ import Message from 'element-ui/lib/message';
import Notification from 'element-ui/lib/notification';
import CollapseTransition from 'element-ui/lib/transitions/collapse-transition';
import N8nActionBox from './N8nActionBox';
import N8nActionToggle from './N8nActionToggle';
import N8nAvatar from './N8nAvatar';
import N8nBadge from './N8nBadge';
import N8nButton from './N8nButton';
import N8nFormBox from './N8nFormBox';
import N8nFormInput from './N8nFormInput';
import N8nFormInputs from './N8nFormInputs';
import N8nHeading from './N8nHeading';
import N8nIcon from './N8nIcon';
import N8nIconButton from './N8nIconButton';
import N8nInput from './N8nInput';
@@ -39,10 +47,11 @@ import N8nInfoTip from './N8nInfoTip';
import N8nInputNumber from './N8nInputNumber';
import N8nInputLabel from './N8nInputLabel';
import N8nLoading from './N8nLoading';
import N8nHeading from './N8nHeading';
import N8nMarkdown from './N8nMarkdown';
import N8nMenu from './N8nMenu';
import N8nMenuItem from './N8nMenuItem';
import N8nLink from './N8nLink';
import N8nOption from './N8nOption';
import N8nSelect from './N8nSelect';
import N8nSpinner from './N8nSpinner';
import N8nSquareButton from './N8nSquareButton';
@@ -50,25 +59,33 @@ import N8nTags from './N8nTags';
import N8nTag from './N8nTag';
import N8nText from './N8nText';
import N8nTooltip from './N8nTooltip';
import N8nOption from './N8nOption';
import N8nUsersList from './N8nUsersList';
import N8nUserSelect from './N8nUserSelect';
import lang from 'element-ui/lib/locale/lang/en';
import locale from 'element-ui/lib/locale';
locale.use(lang);
import locale from '../locale';
export {
N8nActionBox,
N8nActionToggle,
N8nAvatar,
N8nBadge,
N8nButton,
N8nHeading,
N8nFormBox,
N8nFormInput,
N8nFormInputs,
N8nIcon,
N8nIconButton,
N8nInfoTip,
N8nInput,
N8nInputLabel,
N8nInputNumber,
N8nLink,
N8nLoading,
N8nMarkdown,
N8nHeading,
N8nMenu,
N8nMenuItem,
N8nOption,
N8nSelect,
N8nSpinner,
N8nSquareButton,
@@ -76,7 +93,9 @@ export {
N8nTag,
N8nText,
N8nTooltip,
N8nOption,
N8nUsersList,
N8nUserSelect,
Dialog,
Drawer,
Dropdown,
@@ -109,4 +128,6 @@ export {
Message,
Notification,
CollapseTransition,
locale,
};

View File

@@ -0,0 +1,56 @@
const hasOwnProperty = Object.prototype.hasOwnProperty;
export function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key);
};
const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g;
/**
* String format template
* - Inspired:
* https://github.com/ElemeFE/element/blob/dev/src/locale/format.js
* https://github.com/Matt-Esch/string-template/index.js
*/
export default function(Vue) {
/**
* template
*
* @param {String | Function} string
* @param {Array} ...args
* @return {String}
*/
function template(value, ...args) {
if (typeof value === 'function') {
return value(args);
}
const string = value;
if (args.length === 1 && typeof args[0] === 'object') {
args = args[0];
}
if (!args || !args.hasOwnProperty) {
args = {};
}
return string.replace(RE_NARGS, (match, prefix, i, index) => {
let result;
if (string[index - 1] === '{' &&
string[index + match.length] === '}') {
return i;
} else {
result = hasOwn(args, i) ? args[i] : null;
if (result === null || result === undefined) {
return '';
}
return result;
}
});
}
return template;
}

View File

@@ -0,0 +1,45 @@
import defaultLang from '../locale/lang/en';
import Vue from 'vue';
import Format from './format';
import ElementLocale from 'element-ui/lib/locale';
import ElementLang from 'element-ui/lib/locale/lang/en';
ElementLocale.use(ElementLang);
const format = Format(Vue);
let lang = defaultLang;
let i18nHandler;
export const t = function(path, options) {
if (typeof i18nHandler === 'function') {
const value = i18nHandler.apply(this, arguments);
if (value !== null && value !== undefined && value !== path) return value;
}
// only support flat keys
if (lang[path] !== undefined) {
return format(lang[path], options);
}
return '';
};
export const use = function(l) {
try {
const ndsLang = require(`./lang/${l}`);
lang = ndsLang.default;
// todo breaks select empty data
// const elLang = require(`element-ui/lib/locale/lang/${l}`);;
// ElementLocale.use(elLang);
} catch (e) {
}
};
export const i18n = function(fn) {
i18nHandler = fn || i18nHandler;
};
export default { use, t, i18n };

View File

@@ -0,0 +1,19 @@
export default {
'nds.auth.roles.owner': 'Owner',
'nds.userInfo.you': '(you)',
'nds.userSelect.selectUser': 'Select User',
'nds.userSelect.noMatchingUsers': 'No matching users',
'nds.usersList.deleteUser': 'Delete User',
'nds.usersList.reinviteUser': 'Resend invite',
'formInput.validator.fieldRequired': 'This field is required',
'formInput.validator.minCharactersRequired': 'Must be at least {minimum} characters',
'formInput.validator.maxCharactersRequired': 'Must be at most {maximum} characters',
'formInput.validator.oneNumbersRequired': (config) => {
return `Must have at least ${config.minimum} number${config.minimum > 1 ? 's' : ''}`;
},
'formInput.validator.validEmailRequired': 'Must be a valid email',
'formInput.validator.uppercaseCharsRequired': (config) => (`Must have at least ${config.minimum} uppercase character${
config.minimum > 1 ? 's' : ''
}`),
"formInput.validator.defaultPasswordRequirements": "8+ characters, at least 1 number and 1 capital letter",
};

View File

@@ -0,0 +1,9 @@
import { t } from '../locale';
export default {
methods: {
t(...args) {
return t.apply(this, args);
},
},
};

View File

@@ -1,4 +1,5 @@
declare module 'element-ui/lib/button';
declare module 'element-ui/lib/col';
declare module 'element-ui/lib/input';
declare module 'element-ui/lib/tooltip';
declare module 'element-ui/lib/input-number';
@@ -6,6 +7,8 @@ declare module 'element-ui/lib/select';
declare module 'element-ui/lib/option';
declare module 'element-ui/lib/menu';
declare module 'element-ui/lib/menu-item';
declare module 'element-ui/lib/row';
declare module 'element-ui/lib/tag';
declare module 'element-ui/lib/skeleton';
declare module 'element-ui/lib/skeleton-item';

View File

@@ -142,3 +142,15 @@ import ColorCircles from './ColorCircles.vue';
}}
</Story>
</Canvas>
<Canvas>
<Story name="avatar">
{{
template: `<color-circles :colors="['--color-avatar-accent-1', '--color-avatar-accent-2']" />`,
components: {
ColorCircles,
},
}}
</Story>
</Canvas>

View File

@@ -0,0 +1,7 @@
@function lightness($h, $s, $l, $lightness) {
@return hsl(var(#{$h}), var(#{$s}), calc(var(#{$l}) + #{$lightness}));
}
@function saturation($h, $s, $l, $saturation) {
@return hsl(var(#{$h}), calc(var(#{$s}) + #{$saturation}), var(#{$l}));
}