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:
@@ -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",
|
||||
};
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nActionBox from './ActionBox.vue';
|
||||
|
||||
export default N8nActionBox;
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nActionToggle from './ActionToggle.vue';
|
||||
|
||||
export default N8nActionToggle;
|
||||
@@ -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',
|
||||
};
|
||||
89
packages/design-system/src/components/N8nAvatar/Avatar.vue
Normal file
89
packages/design-system/src/components/N8nAvatar/Avatar.vue
Normal 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>
|
||||
3
packages/design-system/src/components/N8nAvatar/index.js
Normal file
3
packages/design-system/src/components/N8nAvatar/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import N8nAvatar from './Avatar.vue';
|
||||
|
||||
export default N8nAvatar;
|
||||
@@ -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 = {};
|
||||
58
packages/design-system/src/components/N8nBadge/Badge.vue
Normal file
58
packages/design-system/src/components/N8nBadge/Badge.vue
Normal 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>
|
||||
3
packages/design-system/src/components/N8nBadge/index.js
Normal file
3
packages/design-system/src/components/N8nBadge/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import N8nBadge from './Badge.vue';
|
||||
|
||||
export default N8nBadge;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
160
packages/design-system/src/components/N8nFormBox/FormBox.vue
Normal file
160
packages/design-system/src/components/N8nFormBox/FormBox.vue
Normal 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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nFormBox from './FormBox.vue';
|
||||
|
||||
export default N8nFormBox;
|
||||
@@ -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',
|
||||
};
|
||||
253
packages/design-system/src/components/N8nFormInput/FormInput.vue
Normal file
253
packages/design-system/src/components/N8nFormInput/FormInput.vue
Normal 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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nFormInput from './FormInput.vue';
|
||||
|
||||
export default N8nFormInput;
|
||||
158
packages/design-system/src/components/N8nFormInput/validators.ts
Normal file
158
packages/design-system/src/components/N8nFormInput/validators.ts
Normal 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;
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nFormInputs from './FormInputs.vue';
|
||||
|
||||
export default N8nFormInputs;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import Icon from './Icon.vue';
|
||||
import N8nIcon from './Icon.vue';
|
||||
|
||||
export default Icon;
|
||||
export default N8nIcon;
|
||||
|
||||
@@ -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 }) => ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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/',
|
||||
};
|
||||
108
packages/design-system/src/components/N8nLink/Link.vue
Normal file
108
packages/design-system/src/components/N8nLink/Link.vue
Normal 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>
|
||||
3
packages/design-system/src/components/N8nLink/index.js
Normal file
3
packages/design-system/src/components/N8nLink/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import N8nLink from './Link.vue';
|
||||
|
||||
export default N8nLink;
|
||||
@@ -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 {
|
||||
|
||||
59
packages/design-system/src/components/N8nRoute/Route.vue
Normal file
59
packages/design-system/src/components/N8nRoute/Route.vue
Normal 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>
|
||||
|
||||
3
packages/design-system/src/components/N8nRoute/index.js
Normal file
3
packages/design-system/src/components/N8nRoute/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import N8nRoute from './Route.vue';
|
||||
|
||||
export default N8nRoute;
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nUserInfo from './UserInfo.vue';
|
||||
|
||||
export default N8nUserInfo;
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nUserSelect from './UserSelect.vue';
|
||||
|
||||
export default N8nUserSelect;
|
||||
@@ -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",
|
||||
};
|
||||
149
packages/design-system/src/components/N8nUsersList/UsersList.vue
Normal file
149
packages/design-system/src/components/N8nUsersList/UsersList.vue
Normal 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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import N8nUsersList from './UsersList.vue';
|
||||
|
||||
export default N8nUsersList;
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import ResizeObserver from './ResizeObserver.vue';
|
||||
|
||||
export default ResizeObserver;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
56
packages/design-system/src/locale/format.js
Normal file
56
packages/design-system/src/locale/format.js
Normal 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;
|
||||
}
|
||||
45
packages/design-system/src/locale/index.js
Normal file
45
packages/design-system/src/locale/index.js
Normal 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 };
|
||||
19
packages/design-system/src/locale/lang/en.js
Normal file
19
packages/design-system/src/locale/lang/en.js
Normal 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",
|
||||
};
|
||||
9
packages/design-system/src/mixins/locale.js
Normal file
9
packages/design-system/src/mixins/locale.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { t } from '../locale';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
t(...args) {
|
||||
return t.apply(this, args);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
7
packages/design-system/src/utils.scss
Normal file
7
packages/design-system/src/utils.scss
Normal 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}));
|
||||
}
|
||||
Reference in New Issue
Block a user