diff --git a/.circleci/config.yml b/.circleci/config.yml index 624c3376d..1d019b235 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,6 +23,17 @@ workflows: # run tests for any commit on any branch, including any tags only: /.*/ + - test-frontend-2: + filters: &filters-frontend-2 + tags: + # frontend-2 prefix only + only: /^frontend\-2.*/ + + - chromatic: + context: + - chromatic + filters: *filters-frontend-2 + - get-version: filters: *filters-allow-all @@ -45,6 +56,11 @@ workflows: requires: - get-version + - docker-build-frontend-2: + filters: *filters-build + requires: + - get-version + - docker-build-webhooks: context: *build-context filters: *filters-build @@ -83,7 +99,9 @@ workflows: type: approval filters: &filters-ignore-main-branch-or-all-tags branches: - ignore: main + ignore: + - main + - frontend-2 tags: ignore: /.*/ @@ -110,6 +128,16 @@ workflows: - docker-build-frontend - pre-commit + - docker-publish-frontend-2: + context: *docker-hub-context + filters: *filters-publish + requires: + - get-version + - publish-approval + - docker-build-frontend-2 + - pre-commit + - test-frontend-2 + - docker-publish-webhooks: context: *docker-hub-context filters: *filters-publish @@ -161,6 +189,7 @@ workflows: only: - main - hotfix* + - frontend-2 tags: only: &filters-tag /^[0-9]+\.[0-9]+\.[0-9]+$/ requires: @@ -168,6 +197,7 @@ workflows: - publish-approval - docker-publish-server - docker-publish-frontend + - docker-publish-frontend-2 - docker-publish-webhooks - docker-publish-file-imports - docker-publish-previews @@ -249,6 +279,9 @@ jobs: paths: - .yarn/cache - .yarn/unplugged + - run: + name: Build public packages + command: yarn build:public - run: name: Run pre-commit command: ./.husky/pre-commit @@ -294,7 +327,6 @@ jobs: - run: name: Install Dependencies command: yarn - # working_directory: 'packages/server' - save_cache: name: Save Yarn Package Cache @@ -335,6 +367,75 @@ jobs: # path: packages/server/coverage/lcov-report # destination: package/server/coverage + test-frontend-2: + docker: + - image: cimg/node:16.15-browsers + resource_class: xlarge + steps: + - checkout + - restore_cache: + name: Restore Yarn Package Cache + keys: + - yarn-packages-server-{{ checksum "yarn.lock" }} + - run: + name: Install Dependencies + command: yarn + + - save_cache: + name: Save Yarn Package Cache + key: yarn-packages-server-{{ checksum "yarn.lock" }} + paths: + - .yarn/cache + - .yarn/unplugged + + - run: + name: Build public packages + command: yarn build:public + + - run: + name: Lint everything + command: yarn lint + working_directory: 'packages/frontend-2' + + - run: + name: Install Playwright + command: cd ~ && npx playwright install --with-deps + + - run: + name: Test via Storybook + command: yarn storybook:test:ci + working_directory: 'packages/frontend-2' + + chromatic: + resource_class: medium+ + docker: + - image: cimg/node:16.15 + steps: + - checkout + - restore_cache: + name: Restore Yarn Package Cache + keys: + - yarn-packages-server-{{ checksum "yarn.lock" }} + - run: + name: Install Dependencies + command: yarn + + - save_cache: + name: Save Yarn Package Cache + key: yarn-packages-server-{{ checksum "yarn.lock" }} + paths: + - .yarn/cache + - .yarn/unplugged + + - run: + name: Build shared packages + command: yarn build:public + + - run: + name: Run chromatic + command: yarn chromatic + working_directory: 'packages/frontend-2' + vulnerability-scan: # snyk can undertake most types of scans through GitHub integration # which does not require integration with the CI @@ -384,6 +485,7 @@ jobs: command: | [[ "${CIRCLE_TAG}" ]] && echo "proceed because tag is set" && exit 0 [[ "${CIRCLE_BRANCH}" == "main" ]] && echo "proceed because main branch" && exit 0 + [[ "${CIRCLE_BRANCH}" == "frontend-2" ]] && echo "proceed because frontend-2 branch" && exit 0 [[ "${IS_DRAFT_PR}" == "TRUE" || -z "${CIRCLE_PULL_REQUEST}" ]] && echo "Should not build because either Draft PR or branch without PR, stopping" && exit 1 echo "proceeding" - setup_remote_docker: @@ -409,6 +511,11 @@ jobs: environment: SPECKLE_SERVER_PACKAGE: frontend + docker-build-frontend-2: + <<: *build-job + environment: + SPECKLE_SERVER_PACKAGE: frontend-2 + docker-build-previews: <<: *build-job environment: @@ -465,6 +572,11 @@ jobs: environment: SPECKLE_SERVER_PACKAGE: frontend + docker-publish-frontend-2: + <<: *publish-job + environment: + SPECKLE_SERVER_PACKAGE: frontend-2 + docker-publish-previews: <<: *publish-job environment: diff --git a/.circleci/publish.sh b/.circleci/publish.sh index b37ab577f..fd2c06c4e 100755 --- a/.circleci/publish.sh +++ b/.circleci/publish.sh @@ -26,7 +26,7 @@ if [[ "${IMAGE_VERSION_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-alpha\.[0-9]+)?$ ]]; th docker push "${DOCKER_IMAGE_TAG}:2" fi else - BRANCH_TAG="${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}" + BRANCH_TAG="branch.${BRANCH_NAME_TRUNCATED}" echo "🏷 Tagging and pushing image as '${DOCKER_IMAGE_TAG}:${BRANCH_TAG}'" docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:${BRANCH_TAG}" docker push "${DOCKER_IMAGE_TAG}:${BRANCH_TAG}" diff --git a/.circleci/publish_helm_chart.sh b/.circleci/publish_helm_chart.sh index 1ddcde445..6cabffd9d 100755 --- a/.circleci/publish_helm_chart.sh +++ b/.circleci/publish_helm_chart.sh @@ -1,39 +1,51 @@ -#!/bin/bash +#!/usr/bin/env bash -set -e +set -eo pipefail -RELEASE_VERSION=${IMAGE_VERSION_TAG} +GIT_REPO=$( pwd ) +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# shellcheck disable=SC1090,SC1091 +source "${SCRIPT_DIR}/common.sh" -echo "Releasing Helm Chart version $RELEASE_VERSION" +RELEASE_VERSION="${IMAGE_VERSION_TAG}" +HELM_STABLE_BRANCH="${HELM_STABLE_BRANCH:-"main"}" + +echo "Releasing Helm Chart version ${RELEASE_VERSION}" git config --global user.email "devops+circleci@speckle.systems" git config --global user.name "CI" -git clone git@github.com:specklesystems/helm.git ~/helm -# before overwriting the chart with the build version, check if the current chart version -# is not newer than the currently build one +git clone git@github.com:specklesystems/helm.git "${HOME}/helm" -CURRENT_VERSION="$(grep ^version ~/helm/charts/speckle-server/Chart.yaml | grep -o '2\..*')" -echo "${CURRENT_VERSION}" +yq e -i ".version = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml" +yq e -i ".appVersion = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml" +yq e -i ".docker_image_tag = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/values.yaml" -.circleci/check_version.py "${CURRENT_VERSION}" "${RELEASE_VERSION}" -if [ $? -eq 1 ] -then - echo "The current helm chart version is newer than the currently built. Exiting" - exit 1 +if [[ -n "${CIRCLE_TAG}" || "${CIRCLE_BRANCH}" == "${HELM_STABLE_BRANCH}" ]]; then + # before overwriting the chart with the build version, check if the current chart version + # is not newer than the currently build one + + CURRENT_VERSION="$(grep ^version "${HOME}/helm/charts/speckle-server/Chart.yaml" | grep -o '2\..*')" + echo "${CURRENT_VERSION}" + + .circleci/check_version.py "${CURRENT_VERSION}" "${RELEASE_VERSION}" + if [ $? -eq 1 ] + then + echo "The current helm chart version '${CURRENT_VERSION}' is newer than the version '${RELEASE_VERSION}' we are attempting to publish. Exiting" + exit 1 + fi + rm -rf "${HOME}/helm/charts/speckle-server" + cp -r "${GIT_REPO}/utils/helm/speckle-server" "${HOME}/helm/charts/speckle-server" +else + # overwrite the name of the chart + yq e -i ".name = \"speckle-server-branch-${BRANCH_NAME_TRUNCATED}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml" + rm -rf "${HOME}/helm/charts/speckle-server-branch-${BRANCH_NAME_TRUNCATED}" + cp -r "${GIT_REPO}/utils/helm/speckle-server" "${HOME}/helm/charts/speckle-server-branch-${BRANCH_NAME_TRUNCATED}" fi -rm -rf ~/helm/charts/speckle-server -cp -r utils/helm/speckle-server ~/helm/charts/speckle-server - -sed -i 's/version: [^\s]*/version: '"${RELEASE_VERSION}"'/g' ~/helm/charts/speckle-server/Chart.yaml -sed -i 's/appVersion: [^\s]*/appVersion: '\""${RELEASE_VERSION}"\"'/g' ~/helm/charts/speckle-server/Chart.yaml - -sed -i 's/docker_image_tag: [^\s]*/docker_image_tag: '"${RELEASE_VERSION}"'/g' ~/helm/charts/speckle-server/values.yaml - cd ~/helm git add . -git commit -m "CircleCI commit" +git commit -m "CircleCI commit for version '${RELEASE_VERSION}'" git push diff --git a/.eslintrc.js b/.eslintrc.js index 5963f699a..3ca47ba4e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,10 +23,13 @@ const config = { ignorePatterns: [ 'node_modules', 'dist', + 'dist-*', 'public', 'events.json', '.*.{ts,js,vue,tsx,jsx}', - 'generated/**/*' + 'generated/**/*', + '.nuxt', + '.output' ] } diff --git a/.gitignore b/.gitignore index 7c88eeefb..d60ba0030 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ packages/server/.vscode/*.log # GitGuardian .cache_ggshield + +storybook-static diff --git a/.graphqlrc b/.graphqlrc index 3a4356aa2..0c330da9f 100644 --- a/.graphqlrc +++ b/.graphqlrc @@ -1,4 +1,8 @@ schema: 'http://localhost:3000/graphql' +extensions: + languageService: + # Cause it's busted + enableValidation: false require: - ts-node/register - tsconfig-paths/register diff --git a/.prettierignore b/.prettierignore index d8a38369c..25b394b75 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,11 +2,19 @@ node_modules build dist dist2 +dist-* coverage .nyc_output packages/server/reports* +packages/preview-service/public/render/**/* packages/objectloader/examples/browser/objectloader.web.js packages/viewer/example/speckleviewer.web.js + +packages/frontend-2/.output +packages/frontend-2/.nuxt +packages/frontend-2/lib/core/nuxt-modules/**/templates/*.js +packages/frontend-2/lib/common/generated/**/* + package-lock.json yarn.lock .yarn @@ -24,4 +32,6 @@ venv .*.{ts,js,vue,tsx,jsx} **/generated/**/* -**/generated/graphql.ts \ No newline at end of file +**/generated/graphql.ts + +storybook-static diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1c1e0ef97..923d80eeb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,7 +8,8 @@ "esbenp.prettier-vscode", "hbenl.vscode-mocha-test-adapter", "ryanluker.vscode-coverage-gutters", - "Vue.volar" + "Vue.volar", + "bradlc.vscode-tailwindcss" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": ["octref.vetur", "vscode.typescript-language-features"] diff --git a/lint-staged.config.js b/lint-staged.config.js index 31cc62749..ad2cdd54b 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -9,7 +9,9 @@ module.exports = { // Filter out generated folder files `**/generated/**/*`, // Filter out types in object loader - '**/packages/objectloader/types/**/*' + '**/packages/objectloader/types/**/*', + // Filter out nuxt plugin templates + '**/templates/plugin.js' ]) return 'eslint --cache --max-warnings=0 ' + finalFiles.join(' ') diff --git a/package.json b/package.json index df480b60d..be591a521 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,10 @@ "dev": "yarn workspaces foreach -piv -j unlimited run dev", "dev:no-server": "yarn workspaces foreach --exclude @speckle/server -piv -j unlimited run dev", "dev:minimal": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/shared}' run dev", - "gqlgen": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend}' run gqlgen", + "gqlgen": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/frontend-2}' run gqlgen", "dev:server": "yarn workspace @speckle/server dev", "dev:frontend": "yarn workspace @speckle/frontend dev", + "dev:frontend-2": "yarn workspace @speckle/frontend-2 dev", "dev:shared": "yarn workspace @speckle/shared dev", "prepare": "husky install", "postinstall": "husky install", @@ -48,7 +49,13 @@ "tslib": "^2.3.1", "core-js": "3.22.4", "graphql": "^15.3.0", - "typescript": "^4.8.4" + "typescript": "^5.0.4", + "prettier": "^2.8.7", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "eslint": "^8.11.0", + "eslint-config-prettier": "^8.5.0", + "@types/react": "file:./packages/frontend-2/type-augmentations/stubs/types__react" }, "config": { "commitizen": { diff --git a/packages/fileimport-service/src/daemon.js b/packages/fileimport-service/src/daemon.js index fdea34d29..b27cff040 100644 --- a/packages/fileimport-service/src/daemon.js +++ b/packages/fileimport-service/src/daemon.js @@ -57,12 +57,16 @@ async function doTask(task) { let fileSizeForMetric = 0 const metricDurationEnd = metricDuration.startTimer() + let newBranchCreated = false + let branchMetadata = { streamId: null, branchName: null } + try { taskLogger.info('Doing task.') const info = await FileUploads().where({ id: task.id }).first() if (!info) { throw new Error('Internal error: DB inconsistent') } + fileTypeForMetric = info.fileType || 'missing_info' fileSizeForMetric = Number(info.fileSize) || 0 taskLogger = taskLogger.child({ @@ -77,6 +81,19 @@ async function doTask(task) { fs.mkdirSync(TMP_INPUT_DIR, { recursive: true }) serverApi = new ServerAPI({ streamId: info.streamId }) + + branchMetadata = { + branchName: info.branchName, + streamId: info.streamId + } + const existingBranch = await serverApi.getBranchByNameAndStreamId({ + streamId: info.streamId, + name: info.branchName + }) + if (!existingBranch) { + newBranchCreated = true + } + const { token } = await serverApi.createToken({ userId: info.userId, name: 'temp upload token', @@ -189,6 +206,13 @@ async function doTask(task) { [err.toString(), task.id] ) metricOperationErrors.labels(fileTypeForMetric).inc() + } finally { + const { streamId, branchName } = branchMetadata + await knex.raw( + `NOTIFY file_import_update, '${task.id}:::${streamId}:::${branchName}:::${ + newBranchCreated ? 1 : 0 + }'` + ) } metricDurationEnd({ op: fileTypeForMetric }) metricInputFileSize.labels(fileTypeForMetric).observe(fileSizeForMetric) diff --git a/packages/frontend-2/.env.example b/packages/frontend-2/.env.example new file mode 100644 index 000000000..c26379cba --- /dev/null +++ b/packages/frontend-2/.env.example @@ -0,0 +1,7 @@ +HOST=0.0.0.0 +PORT=8081 + +NUXT_PUBLIC_API_ORIGIN=http://localhost:3000 + +NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4 +NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems diff --git a/packages/frontend-2/.eslintrc.js b/packages/frontend-2/.eslintrc.js new file mode 100644 index 000000000..cdc4f33c5 --- /dev/null +++ b/packages/frontend-2/.eslintrc.js @@ -0,0 +1,129 @@ +const mainExtends = [ + 'plugin:nuxt/recommended', + 'plugin:vue/vue3-recommended', + 'plugin:storybook/recommended', + 'prettier' +] + +/** @type {import('eslint').Linter.Config} */ +const config = { + env: { + node: true + }, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + parser: '@typescript-eslint/parser', + tsconfigRootDir: __dirname, + project: ['./tsconfig.eslint.json'], + extraFileExtensions: ['.vue'] + }, + extends: [...mainExtends], + plugins: ['@typescript-eslint'], + ignorePatterns: [ + '**/templates/*', + 'coverage', + 'lib/common/generated/**/*', + 'storybook-static', + '!.storybook', + '.nuxt', + '.output' + ], + rules: { + camelcase: [ + 'error', + { + properties: 'always', + allow: ['^[\\w]+_[\\w]+Fragment$'] + } + ], + 'no-alert': 'error', + eqeqeq: ['error', 'always', { null: 'always' }], + 'no-console': 'off', + 'no-var': 'error' + }, + overrides: [ + { + files: '*.test.{ts,js}', + env: { + jest: true + } + }, + { + files: './{components|pages|store|lib}/*.{js,ts,vue}', + env: { + node: false, + browser: true + } + }, + { + files: '*.{ts,tsx,vue}', + extends: ['plugin:@typescript-eslint/recommended', ...mainExtends], + rules: { + '@typescript-eslint/no-explicit-any': ['error'], + '@typescript-eslint/no-unsafe-argument': ['error'], + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-for-in-array': ['error'], + '@typescript-eslint/restrict-template-expressions': ['error'], + '@typescript-eslint/restrict-plus-operands': ['error'], + '@typescript-eslint/await-thenable': ['warn'], + '@typescript-eslint/ban-types': ['warn'], + 'require-await': 'off', + '@typescript-eslint/require-await': 'error', + 'no-undef': 'off' + } + }, + { + files: '*.vue', + plugins: ['vuejs-accessibility'], + extends: [ + 'plugin:@typescript-eslint/recommended', + ...mainExtends, + 'plugin:vuejs-accessibility/recommended' + ], + rules: { + 'vue/component-tags-order': [ + 'error', + { order: ['docs', 'template', 'script', 'style'] } + ], + 'vue/require-default-prop': 'off', + 'vue/multi-word-component-names': 'off', + 'vue/component-name-in-template-casing': [ + 'error', + 'PascalCase', + { registeredComponentsOnly: false } + ], + 'vuejs-accessibility/label-has-for': [ + 'error', + { + required: { + some: ['nesting', 'id'] + } + } + ] + } + }, + { + files: '*.d.ts', + rules: { + 'no-var': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-types': 'off' + } + }, + { + files: '*.stories.{js,ts}', + rules: { + // this one feels busted, tells me to await synchronous calls + 'storybook/await-interactions': 'off', + // storybook types suck and can't be augmented + '@typescript-eslint/no-unsafe-call': 'off' + } + } + ] +} + +module.exports = config diff --git a/packages/frontend-2/.gitignore b/packages/frontend-2/.gitignore new file mode 100644 index 000000000..438cb0860 --- /dev/null +++ b/packages/frontend-2/.gitignore @@ -0,0 +1,8 @@ +node_modules +*.log* +.nuxt +.nitro +.cache +.output +.env +dist diff --git a/packages/frontend-2/.storybook/main.ts b/packages/frontend-2/.storybook/main.ts new file mode 100644 index 000000000..70fdab549 --- /dev/null +++ b/packages/frontend-2/.storybook/main.ts @@ -0,0 +1,76 @@ +import dotenv from 'dotenv' +import Unimport from 'unimport/unplugin' +import { flatten } from 'lodash-es' +import type { StorybookConfig } from '@storybook/vue3-vite' +import { mergeConfig, InlineConfig } from 'vite' +import jiti from 'jiti' + +// used in nuxt.config.ts +process.env.IS_STORYBOOK_BUILD = 'true' + +// make nuxt env vars available here +dotenv.config() + +// having to use jiti cause of weird transpilation stuff going on during the storybook build +const jitiImport = jiti(__filename, { + cache: false, + esmResolve: true +}) +const nuxtViteConfigUtil = jitiImport( + '../lib/fake-nuxt-env/utils/nuxtViteConfig.mjs' +) as typeof import('~~/lib/fake-nuxt-env/utils/nuxtViteConfig.mjs') +const storyPaths = ['stories', 'components', 'pages', 'lib', 'layouts'] +const storiesPairs = storyPaths.map((p) => [ + `../${p}/**/*.stories.mdx`, + `../${p}/**/*.stories.@(js|ts)` +]) +const stories = flatten(storiesPairs) + +/** + * STORYBOOK CONFIG STARTS HERE + */ +const config: StorybookConfig = { + stories, + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/addon-a11y' + ], + framework: { + name: '@storybook/vue3-vite', + options: {} + }, + features: { + storyStoreV7: true + }, + async viteFinal(config) { + const now = performance.now() + console.log('Integrating Nuxt into Storybook...') + const { resolvedViteConfig, nuxt } = + await nuxtViteConfigUtil.integrateNuxtIntoStorybook() + const { unimportOptions } = await nuxtViteConfigUtil.getNuxtModuleConfigs(nuxt) + console.log(`...done [${Math.ceil(performance.now() - now)}ms]`) + const customConfig: InlineConfig = { + plugins: [ + // Auto-imports managed by unimport + // TODO: Is this already handled through nuxtViteConfig? Global functions seem to work without this + Unimport.vite(unimportOptions) + ], + define: { + 'process.server': false, + 'process.client': true + }, + build: { + sourcemap: false + } + } + let final = mergeConfig(config, resolvedViteConfig) + final = mergeConfig(final, customConfig) + return final + }, + docs: { + autodocs: true + } +} +export default config diff --git a/packages/frontend-2/.storybook/preview-head.html b/packages/frontend-2/.storybook/preview-head.html new file mode 100644 index 000000000..7100922b6 --- /dev/null +++ b/packages/frontend-2/.storybook/preview-head.html @@ -0,0 +1,3 @@ + diff --git a/packages/frontend-2/.storybook/preview.js b/packages/frontend-2/.storybook/preview.js new file mode 100644 index 000000000..7086010fc --- /dev/null +++ b/packages/frontend-2/.storybook/preview.js @@ -0,0 +1,209 @@ +import '~~/assets/css/tailwind.css' +import { setupVueApp } from '~~/lib/fake-nuxt-env/utils/nuxtAppBootstrapper' +import { MockedApolloProvider } from '~~/lib/fake-nuxt-env/components/MockedApolloProvider' +import { setup } from '@storybook/vue3' +import SingletonManagers from '~~/components/singleton/Managers.vue' +import { useArgs, useGlobals } from '@storybook/client-api' +import { provide, watch } from 'vue' +import { AppTheme, useTheme } from '~~/lib/core/composables/theme' + +setup((app) => { + setupVueApp(app) +}) + +export const parameters = { + // Main storybook params + viewport: { + viewports: { + mobile1: { + name: 'Small mobile (320px)', + styles: { width: '320px', height: '568px' }, // ratio 0.56 + type: 'mobile' + }, + mobile2: { + name: 'Large mobile (414px)', + styles: { width: '414px', height: '896px' }, // ratio 0.46 + type: 'mobile' + }, + SM: { + name: 'SM (640px)', + styles: { width: '640px', height: '1024px' }, + type: 'mobile' + }, + MD: { + name: 'MD (768px)', + styles: { width: '768px', height: '1024px' }, + type: 'tablet' + }, + LG: { + name: 'LG (1024px)', + styles: { width: '1024px', height: '768px' }, + type: 'desktop' + }, + XL: { + name: 'XL (1280px)', + styles: { width: '1280px', height: '768px' }, + type: 'desktop' + }, + '2XL': { + name: '2XL (1536px)', + styles: { width: '1536px', height: '1024px' }, + type: 'desktop' + } + } + }, + backgrounds: { + // Using tailwind theme bg values + default: 'foundation-page', + values: [ + { + name: 'foundation-page', + value: 'var(--foundation-page)' + }, + { + name: 'foundation', + value: 'var(--foundation)' + }, + { + name: 'foundation-2', + value: 'var(--foundation-2)' + }, + { + name: 'foundation-3', + value: 'var(--foundation-3)' + }, + { + name: 'foundation-4', + value: 'var(--foundation-4)' + }, + { + name: 'foundation-5', + value: 'var(--foundation-5)' + }, + { + name: 'foundation-focus', + value: 'var(--foundation-focus)' + }, + { + name: 'foundation-disabled', + value: 'var(--foundation-disabled)' + } + ] + }, + // Custom params + apolloClient: { + MockedApolloProvider + } +} + +/** @type {import('@storybook/csf').DecoratorFunction[]} */ +export const decorators = [ + // Feed in updateArgs() into stories + (story, ctx) => { + const [, updateArgs] = useArgs() + return story({ ...ctx, updateArgs }) + }, + + /** + * - Global CSS class setup + * - Theme support + * - Global singletons + */ + (story, ctx) => { + const [, updateGlobals] = useGlobals() + const theme = ctx.globals.theme + const isDarkMode = theme === 'dark' + const { + parameters: { manualLayout } + } = ctx + + if (isDarkMode) { + document.querySelector('html').classList.add('dark') + } else { + document.querySelector('html').classList.remove('dark') + } + + return { + components: { + Story: story(), + SingletonManagers + }, + setup: () => { + const { isDarkTheme, setTheme } = useTheme() + + setTheme(isDarkMode ? AppTheme.Dark : AppTheme.Light) + + watch(isDarkTheme, (isDark, oldIsDark) => { + if (isDark === oldIsDark) return + + updateGlobals({ + theme: isDark ? 'dark' : 'light' + }) + }) + }, + inheritAttrs: false, + template: ` +
+ + ${manualLayout ? '' : ''} +
+ ` + } + }, + // Apollo Mocked Provider decorator + (story, ctx) => { + const { + parameters: { + apolloClient: { MockedApolloProvider, ...providerProps } + } + } = ctx + + if (!MockedApolloProvider) { + console.error( + 'Apollo MockedApolloProvider missing from parameters in preview.js!' + ) + return { template: ``, components: { Story: story() } } + } + + return { + data: () => ({ providerProps }), + components: { MockedApolloProvider, Story: story() }, + template: ` + + ` + } + }, + // Mocked router + (story, ctx) => { + const { + parameters: { vueRouter: { route } = { route: undefined } } + } = ctx + + return { + components: { Story: story() }, + setup: () => { + if (route) { + provide('_route', route) + } + }, + template: `` + } + } +] + +export const globalTypes = { + theme: { + name: 'Theme', + description: 'Global theme for components', + defaultValue: 'light', + toolbar: { + icon: 'circlehollow', + // Array of plain string values or MenuItem shape (see below) + items: ['light', 'dark'], + // Property that specifies if the name of the item will be displayed + title: 'Theme', + // Change title based on selected value + dynamicTitle: true + } + } +} diff --git a/packages/frontend-2/.vscode/settings.json b/packages/frontend-2/.vscode/settings.json new file mode 100644 index 000000000..4ef7f0183 --- /dev/null +++ b/packages/frontend-2/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "css.validate": false, + "less.validate": false, + "scss.validate": false, + "stylelint.validate": ["css", "scss", "vue", "postcss"], + "stylelint.enable": true, + "stylelint.configFile": "${workspaceFolder}/stylelint.config.js", + "volar.completion.preferredTagNameCase": "pascal", + "javascript.suggest.autoImports": true, + "typescript.suggest.autoImports": true, + "typescript.preferences.importModuleSpecifier": "non-relative", + "javascript.preferences.importModuleSpecifier": "non-relative" +} diff --git a/packages/frontend-2/Dockerfile b/packages/frontend-2/Dockerfile new file mode 100644 index 000000000..77682fd89 --- /dev/null +++ b/packages/frontend-2/Dockerfile @@ -0,0 +1,47 @@ +FROM node:16.18.0-bullseye-slim as build-stage +ARG NODE_ENV=production +ARG SPECKLE_SERVER_VERSION=custom + +WORKDIR /speckle-server + +COPY .yarnrc.yml . +COPY .yarn ./.yarn +COPY package.json yarn.lock ./ + +COPY packages/viewer/package.json ./packages/viewer/ +COPY packages/objectloader/package.json ./packages/objectloader/ +COPY packages/shared/package.json ./packages/shared/ +COPY packages/frontend-2/package.json ./packages/frontend-2/ +COPY packages/frontend-2/type-augmentations ./packages/frontend-2/ + +COPY packages/objectloader ./packages/objectloader/ +COPY packages/viewer ./packages/viewer/ +COPY packages/shared ./packages/shared/ +COPY packages/frontend-2 ./packages/frontend-2/ + +RUN yarn workspaces focus -A +# hadolint ignore=DL3059 +RUN yarn workspaces foreach run build + +FROM node:16.18.0-bullseye-slim as production-stage +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +ENV TINI_VERSION v0.19.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini +RUN chmod +x /tini +ENTRYPOINT ["/tini", "--"] + +USER node + +ENV PORT=8080 + +ENV NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4 +ENV NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems + +WORKDIR /speckle-server +COPY --from=build-stage /speckle-server/packages/frontend-2/.output . + +EXPOSE ${PORT} + +CMD ["node", "./server/index.mjs"] diff --git a/packages/frontend-2/app.vue b/packages/frontend-2/app.vue new file mode 100644 index 000000000..2dd2abb7b --- /dev/null +++ b/packages/frontend-2/app.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/frontend-2/assets/css/tailwind-setup/semantic-colors.css b/packages/frontend-2/assets/css/tailwind-setup/semantic-colors.css new file mode 100644 index 000000000..df3105062 --- /dev/null +++ b/packages/frontend-2/assets/css/tailwind-setup/semantic-colors.css @@ -0,0 +1,124 @@ +/* stylelint-disable custom-property-empty-line-before */ +/* stylelint-disable comment-empty-line-before */ +/* + Currently not possible to define different color values for the same color name just through + the tailwind config, so using CSS variables instead. + + Palette reference: https://tailwindcss.com/docs/customizing-colors + */ + +:root { + /* used only as the page background */ + --foundation-page: #f1f5f9; + /* used as the background for any elements that sit on the page */ + --foundation: #fcfcfc; + --foundation-2: #fcfcfc; + --foundation-3: #fcfcfc; + --foundation-4: #fcfcfc; + --foundation-5: #fcfcfc; + /* for hover/focus states */ + --foundation-focus: #dbeafe; + /* for disabled backgrounds */ + --foundation-disabled: #e5e5e5; + + /* default foreground color */ + --foreground: #334155; + /* dimmer foreground color, e.g. caption text */ + --foreground-2: #94a3b8; + /* disabled foreground color */ + --foreground-disabled: #a3a3a3; + /* primary color when used for text directly on top of foundation-page */ + --foreground-primary: #3b82f6; + /* foreground color when put on top of a primary colored background */ + --foreground-on-primary: #fff; + + /* primary color */ + --primary: #3b82f6; + /* focused primary color */ + --primary-focus: #2563eb; + /* muted primary color */ + --primary-muted: #3b82f60d; + + /* outline variations */ + --outline-1: #3b82f6; + --outline-2: #93c5fd; + --outline-3: #cbd5e1; + + /* success variations */ + --success: #34d399; + --success-lighter: #d1fae5; + --success-darker: #064e3b; + + /* warning variations */ + --warning: #fbbf24; + --warning-lighter: #fef3c7; + --warning-darker: #78350f; + + /* info variations */ + --info: #38bdf8; + --info-lighter: #e0f2fe; + --info-darker: #0c4a6e; + + /* danger variations */ + --danger: #f87171; + --danger-lighter: #fee2e2; + --danger-darker: #7f1d1d; +} + +:root.dark { + /* used only as the page background */ + --foundation-page: #18181b; + /* used as the background for any element that sits on the page */ + --foundation: #27272a; + --foundation-2: #303034; + --foundation-3: #52525b; + --foundation-4: #71717a; + --foundation-5: #a1a1aa; + /* for hover/focus states */ + --foundation-focus: #52525b; + /* for disabled backgrounds */ + --foundation-disabled: #3c3c3d; + + /* default foreground color */ + --foreground: #f4f4f5; + /* dimmer foreground color, e.g. caption text */ + --foreground-2: #71717a; + /* disabled foreground color */ + --foreground-disabled: #5a5a5f; + /* primary color when used for text directly on top of foundation-page */ + --foreground-primary: #bfdbfe; + /* foreground color when put on top of a primary colored background */ + --foreground-on-primary: #fafafa; + + /* primary color */ + --primary: #3b82f6; + /* focused primary color */ + --primary-focus: #60a5fa; + /* muted primary color */ + --primary-muted: #71717a0d; + + /* outline variations */ + --outline-1: #a1a1aa; + --outline-2: #52525b; + --outline-3: #3f3f46; + + /* success variations */ + --success: #34d399; + --success-lighter: #a7f3d0; + --success-darker: #064e3b; + + /* warning variations */ + --warning: #facc15; + --warning-lighter: #fef08a; + --warning-darker: #78350f; + + /* info variations */ + --info: #38bdf8; + --info-lighter: #bae6fd; + --info-darker: #0c4a6e; + + /* danger variations */ + --danger: #f87171; + --danger-lighter: #fecaca; + --danger-darker: #7f1d1d; +} diff --git a/packages/frontend-2/assets/css/tailwind-setup/tippy.css b/packages/frontend-2/assets/css/tailwind-setup/tippy.css new file mode 100644 index 000000000..d20f05c33 --- /dev/null +++ b/packages/frontend-2/assets/css/tailwind-setup/tippy.css @@ -0,0 +1,15 @@ +/** + * Tippy mentions theme + */ +.tippy-box[data-theme='mention'] { + background: none; + pointer-events: none; + + .tippy-arrow { + display: none; + } + + .tippy-content > * { + pointer-events: auto; + } +} diff --git a/packages/frontend-2/assets/css/tailwind.css b/packages/frontend-2/assets/css/tailwind.css new file mode 100644 index 000000000..c5759bc4b --- /dev/null +++ b/packages/frontend-2/assets/css/tailwind.css @@ -0,0 +1,155 @@ +/* stylelint-disable selector-id-pattern */ +@import './tailwind-setup/semantic-colors.css'; +@import './tailwind-setup/tippy.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/** + * Don't pollute this - it's going to be bundled in all pages! + */ + +@layer components { + /** + * Heading weights & line heights can differ, which is why we only set the font size + */ + .h1 { + @apply text-5xl leading-10; + } + + .h2 { + @apply text-4xl leading-10; + } + + .h3 { + @apply text-3xl leading-9; + } + + .h4 { + @apply text-2xl leading-8; + } + + .h5 { + @apply text-xl leading-7; + } + + .h6 { + @apply text-lg leading-6; + } + + .label { + @apply text-sm font-medium leading-5; + + &--light { + @apply font-normal; + } + } + + .label-light { + @apply label label--light; + } + + .normal { + @apply text-base font-normal; + } + + .caption { + @apply text-xs; + } + + .text-tiny { + font-size: 0.6rem; + line-height: 1rem; + } + + /** + * Grid/Layout container that limits max width to expected sizes that we use in our designs + * (see Figma - Design System - Foundations - Grid & Layout) + */ + .layout-container { + @apply mx-auto; + + /* base/mobile - fluid, no max width, just padding */ + @apply px-4; + + /* sm+ - also fluid, increased padding */ + @media (min-width: theme('screens.sm')) { + @apply px-8; + } + + /* lg+ (from this point on, no padding just limited max width) */ + @media (min-width: theme('screens.lg')) { + /* @apply px-0 max-w-[928px]; */ + @apply px-6 max-w-full; + } + + /* xl+ */ + @media (min-width: theme('screens.xl')) { + @apply max-w-[1216px]; + } + + /* 2xl+ */ + @media (min-width: theme('screens.2xl')) { + @apply max-w-[1312px]; + } + } + + /* Simple scrollbar (OSX-like) to use instead of the ugly browser one */ + .simple-scrollbar { + scrollbar-width: var(--simple-scrollbar-width); + scrollbar-color: var(--foreground-2); + + &::-webkit-scrollbar { + width: var(--simple-scrollbar-width); + height: 6px; + } + + &::-webkit-scrollbar-track { + border-radius: 15px; + background: var(--foundation-disabled); + } + + &::-webkit-scrollbar-thumb { + border-radius: 15px; + background: var(--foreground-2); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--foreground-2); + } + + &::-webkit-scrollbar-thumb:active { + background: rgba(90 90 90 10100%); + } + } +} + +:root { + --simple-scrollbar-width: 4px; +} + +/** + * Making sure page is always stretched to the bottom of the screen even if there's nothing in it + */ +html, +body, +div#__nuxt, +div#__nuxt > div { + min-height: 100%; +} + +html, +body, +div#__nuxt { + height: 100%; +} + +body { + @apply font-sans; +} + +[type='checkbox']:focus, +[type='radio']:focus { + @apply ring-offset-foundation; +} diff --git a/packages/frontend-2/assets/images/auth/github_icon.svg b/packages/frontend-2/assets/images/auth/github_icon.svg new file mode 100644 index 000000000..aa05db9c5 --- /dev/null +++ b/packages/frontend-2/assets/images/auth/github_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/frontend-2/assets/images/auth/google_icon.svg b/packages/frontend-2/assets/images/auth/google_icon.svg new file mode 100644 index 000000000..7bb36f977 --- /dev/null +++ b/packages/frontend-2/assets/images/auth/google_icon.svg @@ -0,0 +1,45 @@ + + + + btn_google_light_normal_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend-2/assets/images/auth/google_icon_w_bg.svg b/packages/frontend-2/assets/images/auth/google_icon_w_bg.svg new file mode 100644 index 000000000..4dd789573 --- /dev/null +++ b/packages/frontend-2/assets/images/auth/google_icon_w_bg.svg @@ -0,0 +1,35 @@ + + + + btn_google_light_normal_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend-2/assets/images/auth/ms_icon.svg b/packages/frontend-2/assets/images/auth/ms_icon.svg new file mode 100644 index 000000000..1f7397648 --- /dev/null +++ b/packages/frontend-2/assets/images/auth/ms_icon.svg @@ -0,0 +1 @@ +MS-SymbolLockup \ No newline at end of file diff --git a/packages/frontend-2/assets/images/boxes/empty.png b/packages/frontend-2/assets/images/boxes/empty.png new file mode 100755 index 000000000..399a9bb54 Binary files /dev/null and b/packages/frontend-2/assets/images/boxes/empty.png differ diff --git a/packages/frontend-2/assets/images/comments_intro_320x248.webp b/packages/frontend-2/assets/images/comments_intro_320x248.webp new file mode 100644 index 000000000..4848bffc8 Binary files /dev/null and b/packages/frontend-2/assets/images/comments_intro_320x248.webp differ diff --git a/packages/frontend-2/assets/images/speckle_logo_big.png b/packages/frontend-2/assets/images/speckle_logo_big.png new file mode 100644 index 000000000..c2a7002af Binary files /dev/null and b/packages/frontend-2/assets/images/speckle_logo_big.png differ diff --git a/packages/frontend-2/assets/images/speckle_text_logo_white.svg b/packages/frontend-2/assets/images/speckle_text_logo_white.svg new file mode 100644 index 000000000..8806b2f46 --- /dev/null +++ b/packages/frontend-2/assets/images/speckle_text_logo_white.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend-2/codegen.ts b/packages/frontend-2/codegen.ts new file mode 100644 index 000000000..1614f18a4 --- /dev/null +++ b/packages/frontend-2/codegen.ts @@ -0,0 +1,28 @@ +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + schema: 'http://127.0.0.1:3000/graphql', + documents: ['{lib,components,layouts,pages,middleware}/**/*.{vue,js,ts}'], + ignoreNoDocuments: true, // for better experience with the watcher + generates: { + './lib/common/generated/gql/': { + preset: 'client', + config: { + useTypeImports: true, + fragmentMasking: false, + dedupeFragments: true, + scalars: { + JSONObject: '{}', + DateTime: 'string' + } + }, + presetConfig: { + fragmentMasking: false, + dedupeFragments: true + }, + plugins: [] + } + } +} + +export default config diff --git a/packages/frontend-2/components/ActiveUserTest.vue b/packages/frontend-2/components/ActiveUserTest.vue new file mode 100644 index 000000000..10c18e218 --- /dev/null +++ b/packages/frontend-2/components/ActiveUserTest.vue @@ -0,0 +1,27 @@ + + diff --git a/packages/frontend-2/components/DemoNuxtFunctionality.stories.ts b/packages/frontend-2/components/DemoNuxtFunctionality.stories.ts new file mode 100644 index 000000000..a5c5a1ed4 --- /dev/null +++ b/packages/frontend-2/components/DemoNuxtFunctionality.stories.ts @@ -0,0 +1,22 @@ +import DemoNuxtFunctionality from '~~/components/DemoNuxtFunctionality.vue' +import { StoryObj, Meta } from '@storybook/vue3' + +export default { + title: 'Appendix/Test/Demo Nuxt Functionality (test)', + component: DemoNuxtFunctionality, + parameters: { + docs: { + description: { + component: + 'This invokes various Nuxt globals (funcs & Vue components) as a test to ensure the Nuxt-Storybook integration works' + } + } + } +} as Meta + +export const Default: StoryObj = { + render: () => ({ + components: { DemoNuxtFunctionality }, + template: `` + }) +} diff --git a/packages/frontend-2/components/DemoNuxtFunctionality.vue b/packages/frontend-2/components/DemoNuxtFunctionality.vue new file mode 100644 index 000000000..d05cfec31 --- /dev/null +++ b/packages/frontend-2/components/DemoNuxtFunctionality.vue @@ -0,0 +1,20 @@ + + diff --git a/packages/frontend-2/components/DesignSystem.stories.ts b/packages/frontend-2/components/DesignSystem.stories.ts new file mode 100644 index 000000000..0513925c2 --- /dev/null +++ b/packages/frontend-2/components/DesignSystem.stories.ts @@ -0,0 +1,18 @@ +import { Meta, StoryObj } from '@storybook/vue3' +import DesignSystem from '~~/components/DesignSystem.vue' + +export default { + title: 'Overview/Styling/Design System Examples', + component: DesignSystem +} as Meta + +export const Default: StoryObj = { + render: () => ({ + components: { DesignSystem }, + template: ` +
+ +
+ ` + }) +} diff --git a/packages/frontend-2/components/DesignSystem.vue b/packages/frontend-2/components/DesignSystem.vue new file mode 100644 index 000000000..61407cff3 --- /dev/null +++ b/packages/frontend-2/components/DesignSystem.vue @@ -0,0 +1,436 @@ + + diff --git a/packages/frontend-2/components/auth/LoginPanel.vue b/packages/frontend-2/components/auth/LoginPanel.vue new file mode 100644 index 000000000..bdde2af97 --- /dev/null +++ b/packages/frontend-2/components/auth/LoginPanel.vue @@ -0,0 +1,50 @@ + + diff --git a/packages/frontend-2/components/auth/LoginWithEmailBlock.vue b/packages/frontend-2/components/auth/LoginWithEmailBlock.vue new file mode 100644 index 000000000..8aa432739 --- /dev/null +++ b/packages/frontend-2/components/auth/LoginWithEmailBlock.vue @@ -0,0 +1,87 @@ + + diff --git a/packages/frontend-2/components/auth/PasswordChecks.vue b/packages/frontend-2/components/auth/PasswordChecks.vue new file mode 100644 index 000000000..882215d06 --- /dev/null +++ b/packages/frontend-2/components/auth/PasswordChecks.vue @@ -0,0 +1,54 @@ + + diff --git a/packages/frontend-2/components/auth/PasswordResetFinalizationPanel.vue b/packages/frontend-2/components/auth/PasswordResetFinalizationPanel.vue new file mode 100644 index 000000000..16fead661 --- /dev/null +++ b/packages/frontend-2/components/auth/PasswordResetFinalizationPanel.vue @@ -0,0 +1,57 @@ + + diff --git a/packages/frontend-2/components/auth/PasswordResetPanel.vue b/packages/frontend-2/components/auth/PasswordResetPanel.vue new file mode 100644 index 000000000..9d0dd0724 --- /dev/null +++ b/packages/frontend-2/components/auth/PasswordResetPanel.vue @@ -0,0 +1,43 @@ + + diff --git a/packages/frontend-2/components/auth/RegisterPanel.vue b/packages/frontend-2/components/auth/RegisterPanel.vue new file mode 100644 index 000000000..b216c6538 --- /dev/null +++ b/packages/frontend-2/components/auth/RegisterPanel.vue @@ -0,0 +1,79 @@ + + diff --git a/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue b/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue new file mode 100644 index 000000000..885324cbf --- /dev/null +++ b/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue @@ -0,0 +1,136 @@ + + + + diff --git a/packages/frontend-2/components/auth/VerificationReminder.vue b/packages/frontend-2/components/auth/VerificationReminder.vue new file mode 100644 index 000000000..3e5f4a128 --- /dev/null +++ b/packages/frontend-2/components/auth/VerificationReminder.vue @@ -0,0 +1,119 @@ + + diff --git a/packages/frontend-2/components/auth/VerificationReminderMenuNotice.vue b/packages/frontend-2/components/auth/VerificationReminderMenuNotice.vue new file mode 100644 index 000000000..c3e2a402d --- /dev/null +++ b/packages/frontend-2/components/auth/VerificationReminderMenuNotice.vue @@ -0,0 +1,118 @@ + + diff --git a/packages/frontend-2/components/auth/third-party/LoginBlock.vue b/packages/frontend-2/components/auth/third-party/LoginBlock.vue new file mode 100644 index 000000000..d9500ac90 --- /dev/null +++ b/packages/frontend-2/components/auth/third-party/LoginBlock.vue @@ -0,0 +1,95 @@ + + diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonBase.vue b/packages/frontend-2/components/auth/third-party/LoginButtonBase.vue new file mode 100644 index 000000000..78b66ed08 --- /dev/null +++ b/packages/frontend-2/components/auth/third-party/LoginButtonBase.vue @@ -0,0 +1,21 @@ + + diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonGithub.vue b/packages/frontend-2/components/auth/third-party/LoginButtonGithub.vue new file mode 100644 index 000000000..90f0226c7 --- /dev/null +++ b/packages/frontend-2/components/auth/third-party/LoginButtonGithub.vue @@ -0,0 +1,15 @@ + + diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonGoogle.vue b/packages/frontend-2/components/auth/third-party/LoginButtonGoogle.vue new file mode 100644 index 000000000..7ad6f23da --- /dev/null +++ b/packages/frontend-2/components/auth/third-party/LoginButtonGoogle.vue @@ -0,0 +1,15 @@ + + diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonMicrosoft.vue b/packages/frontend-2/components/auth/third-party/LoginButtonMicrosoft.vue new file mode 100644 index 000000000..ace9791c5 --- /dev/null +++ b/packages/frontend-2/components/auth/third-party/LoginButtonMicrosoft.vue @@ -0,0 +1,11 @@ + + diff --git a/packages/frontend-2/components/common/Badge.stories.ts b/packages/frontend-2/components/common/Badge.stories.ts new file mode 100644 index 000000000..fe3336fd2 --- /dev/null +++ b/packages/frontend-2/components/common/Badge.stories.ts @@ -0,0 +1,84 @@ +import { Meta, StoryObj } from '@storybook/vue3' +import CommonBadge from '~~/components/common/Badge.vue' +import { XMarkIcon } from '@heroicons/vue/20/solid' +import { FaceSmileIcon } from '@heroicons/vue/24/outline' +export default { + component: CommonBadge, + argTypes: { + default: { + type: 'string', + description: 'Default slot holds badge text' + }, + iconLeft: { + type: 'function' + }, + size: { + options: ['base', 'lg'], + control: { type: 'select' } + }, + clickIcon: { + action: 'click', + type: 'function' + } + } +} as Meta + +export const Default: StoryObj = { + render: (args) => ({ + components: { CommonBadge }, + setup: () => ({ args }), + template: `{{ args.default || 'Badge' }}` + }), + args: { + size: 'base', + dot: false, + rounded: false, + clickableIcon: false + } +} + +export const Large: StoryObj = { + ...Default, + args: { + size: 'lg' + } +} + +export const Rounded: StoryObj = { + ...Default, + args: { + rounded: true + } +} + +export const WithDot: StoryObj = { + ...Default, + args: { + dot: true + } +} + +export const WithIcon: StoryObj = { + ...Default, + args: { + iconLeft: FaceSmileIcon + } +} + +export const WithClickableIcon: StoryObj = { + ...Default, + args: { + clickableIcon: true, + iconLeft: XMarkIcon + } +} + +export const WithCustomColors: StoryObj = { + ...Default, + args: { + iconLeft: FaceSmileIcon, + dot: true, + colorClasses: 'text-info bg-warning', + dotIconColorClasses: 'text-success' + } +} diff --git a/packages/frontend-2/components/common/Badge.vue b/packages/frontend-2/components/common/Badge.vue new file mode 100644 index 000000000..781ff8659 --- /dev/null +++ b/packages/frontend-2/components/common/Badge.vue @@ -0,0 +1,115 @@ + + diff --git a/packages/frontend-2/components/common/EmptySearchState.vue b/packages/frontend-2/components/common/EmptySearchState.vue new file mode 100644 index 000000000..3348c68c1 --- /dev/null +++ b/packages/frontend-2/components/common/EmptySearchState.vue @@ -0,0 +1,16 @@ + + diff --git a/packages/frontend-2/components/common/loading/Bar.vue b/packages/frontend-2/components/common/loading/Bar.vue new file mode 100644 index 000000000..f691a31d3 --- /dev/null +++ b/packages/frontend-2/components/common/loading/Bar.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/frontend-2/components/common/model/Select.vue b/packages/frontend-2/components/common/model/Select.vue new file mode 100644 index 000000000..9ec30c6ef --- /dev/null +++ b/packages/frontend-2/components/common/model/Select.vue @@ -0,0 +1,146 @@ + + diff --git a/packages/frontend-2/components/common/steps/Bullet.stories.ts b/packages/frontend-2/components/common/steps/Bullet.stories.ts new file mode 100644 index 000000000..225464c8c --- /dev/null +++ b/packages/frontend-2/components/common/steps/Bullet.stories.ts @@ -0,0 +1,107 @@ +import { action } from '@storybook/addon-actions' +import { Meta, StoryObj } from '@storybook/vue3' +import CommonStepsBullet from '~~/components/common/steps/Bullet.vue' +import { BulletStepType } from '~~/lib/common/helpers/components' +import { mergeStories } from '~~/lib/common/helpers/storybook' +import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind' + +type StoryType = StoryObj< + Record & { + 'update:modelValue': (val: boolean) => void + } +> + +const testSteps: BulletStepType[] = [ + { + name: 'First step', + onClick: action('step-clicked') + }, + { + name: 'Second step', + onClick: action('step-clicked') + }, + { + name: 'Third step', + onClick: action('step-clicked') + } +] + +export default { + component: CommonStepsBullet, + argTypes: { + orientation: { + options: ['horizontal', 'vertical'], + control: { type: 'select' } + }, + 'update:modelValue': { + type: 'function', + action: 'v-model' + } + }, + parameters: { + docs: { + description: { + component: 'Bullet-based steps component' + } + } + } +} as Meta + +export const Default: StoryType = { + render: (args, ctx) => ({ + components: { CommonStepsBullet }, + setup: () => ({ args }), + template: ``, + methods: { + onModelUpdate(val: boolean) { + args['update:modelValue'](val) + ctx.updateArgs({ ...args, modelValue: val }) + } + } + }), + args: { + ariaLabel: 'Steps ARIA title!', + basic: false, + orientation: 'horizontal', + steps: testSteps, + modelValue: 1, + nonInteractive: false + } +} + +export const Vertical: StoryType = mergeStories(Default, { + args: { + orientation: 'vertical' + } +}) + +export const VersionBasic: StoryType = mergeStories(Default, { + args: { + basic: true + } +}) + +export const StartOnNegativeStep: StoryType = mergeStories(Default, { + args: { + modelValue: -1 + }, + parameters: { + docs: { + description: { + story: 'Start on -1 step (on neither of the steps)' + } + } + } +}) + +export const GoVerticalBelowBreakpoint: StoryType = mergeStories(Default, { + args: { + goVerticalBelow: TailwindBreakpoints.md + } +}) + +export const NonInteractive: StoryType = mergeStories(Default, { + args: { + nonInteractive: true + } +}) diff --git a/packages/frontend-2/components/common/steps/Bullet.vue b/packages/frontend-2/components/common/steps/Bullet.vue new file mode 100644 index 000000000..a58646cf5 --- /dev/null +++ b/packages/frontend-2/components/common/steps/Bullet.vue @@ -0,0 +1,107 @@ + + + diff --git a/packages/frontend-2/components/common/steps/Number.stories.ts b/packages/frontend-2/components/common/steps/Number.stories.ts new file mode 100644 index 000000000..7ef9de6e9 --- /dev/null +++ b/packages/frontend-2/components/common/steps/Number.stories.ts @@ -0,0 +1,119 @@ +import { action } from '@storybook/addon-actions' +import { Meta, StoryObj } from '@storybook/vue3' +import CommonStepsNumber from '~~/components/common/steps/Number.vue' +import { NumberStepType } from '~~/lib/common/helpers/components' +import { mergeStories } from '~~/lib/common/helpers/storybook' +import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind' + +type StoryType = StoryObj< + Record & { + 'update:modelValue': (val: boolean) => void + } +> + +const testStepsWithDescription: NumberStepType[] = [ + { + name: 'First step', + onClick: action('step-clicked'), + description: 'Some example text' + }, + { + name: 'Second step', + onClick: action('step-clicked'), + description: 'More example text' + }, + { + name: 'Third step', + onClick: action('step-clicked'), + description: 'Final example text' + } +] + +const testStepsWithoutDescription: NumberStepType[] = [ + { + name: 'First step', + onClick: action('step-clicked') + }, + { + name: 'Second step', + onClick: action('step-clicked') + }, + { + name: 'Third step', + onClick: action('step-clicked') + } +] + +export default { + component: CommonStepsNumber, + argTypes: { + orientation: { + options: ['horizontal', 'vertical'], + control: { type: 'select' } + }, + 'update:modelValue': { + type: 'function', + action: 'v-model' + } + }, + parameters: { + docs: { + description: { + component: 'Number-based steps component. Also supports optional description.' + } + } + } +} as Meta + +export const Default: StoryType = { + render: (args, ctx) => ({ + components: { CommonStepsNumber }, + setup: () => ({ args }), + template: ``, + methods: { + onModelUpdate(val: boolean) { + args['update:modelValue'](val) + ctx.updateArgs({ ...args, modelValue: val }) + } + } + }), + args: { + ariaLabel: 'Steps ARIA title!', + orientation: 'horizontal', + steps: testStepsWithDescription, + modelValue: 1 + } +} + +export const Vertical: StoryType = mergeStories(Default, { + args: { + orientation: 'vertical' + } +}) + +export const StartOnNegativeStep: StoryType = mergeStories(Default, { + args: { + modelValue: -1 + }, + parameters: { + docs: { + description: { + story: 'Start on -1 step (on neither of the steps)' + } + } + } +}) + +export const NoDescription: StoryType = { + ...Default, + args: { + ...Default.args, + steps: testStepsWithoutDescription + } +} + +export const GoVerticalBelowBreakpoint: StoryType = mergeStories(Default, { + args: { + goVerticalBelow: TailwindBreakpoints.md + } +}) diff --git a/packages/frontend-2/components/common/steps/Number.vue b/packages/frontend-2/components/common/steps/Number.vue new file mode 100644 index 000000000..d565e739d --- /dev/null +++ b/packages/frontend-2/components/common/steps/Number.vue @@ -0,0 +1,106 @@ + + diff --git a/packages/frontend-2/components/common/text/Link.stories.ts b/packages/frontend-2/components/common/text/Link.stories.ts new file mode 100644 index 000000000..fed364095 --- /dev/null +++ b/packages/frontend-2/components/common/text/Link.stories.ts @@ -0,0 +1,139 @@ +import { userEvent, within } from '@storybook/testing-library' +import { Meta, StoryObj } from '@storybook/vue3' +import { wait } from '@speckle/shared' +import CommonTextLink from '~~/components/common/text/Link.vue' +import { mergeStories, VuePlayFunction } from '~~/lib/common/helpers/storybook' + +export default { + component: CommonTextLink, + parameters: { + docs: { + description: { + component: 'Basically just a wrapper over FormButton w/ type link' + } + } + }, + argTypes: { + to: { + type: 'string' + }, + default: { + type: 'string', + description: 'Default slot holds button contents' + }, + click: { + action: 'click', + type: 'function' + }, + external: { + type: 'boolean' + }, + disabled: { + type: 'boolean' + }, + size: { + options: ['xs', 'sm', 'base', 'lg', 'xl'], + control: { type: 'select' } + } + } +} as Meta + +const clickPlayBuilder: (rightClick: boolean) => VuePlayFunction = + (rightClick) => + async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('link') + + userEvent.click(button, rightClick ? { button: 2 } : undefined) + + await wait(1000) + + userEvent.tab() + } +const rightClickPlay = clickPlayBuilder(true) +const leftClickPlay = clickPlayBuilder(false) + +export const Default: StoryObj = { + render: (args) => ({ + components: { CommonTextLink }, + setup() { + return { args } + }, + template: `{{ args.default || 'Link' }}` + }), + play: rightClickPlay, + args: { + to: 'https://google.com', + disabled: false, + default: 'Click me!', + size: 'base' + }, + parameters: { + docs: { + source: { + code: 'Hello World!' + } + } + } +} + +export const Small: StoryObj = mergeStories(Default, { + args: { + size: 'sm' + } +}) + +export const ExtraSmall: StoryObj = mergeStories(Default, { + args: { + size: 'xs' + } +}) + +export const Large: StoryObj = mergeStories(Default, { + args: { + size: 'lg' + } +}) + +export const ExtraLarge: StoryObj = mergeStories(Default, { + args: { + size: 'xl' + } +}) + +export const Disabled: StoryObj = mergeStories(Default, { + play: leftClickPlay, + args: { + disabled: true + }, + parameters: { + docs: { + description: { + story: 'Button is disabled and no mouse events fire' + } + } + } +}) + +export const NoTarget: StoryObj = mergeStories(Default, { + play: leftClickPlay, + args: { + to: null, + default: 'No URL, only for tracking click events' + } +}) + +export const External: StoryObj = mergeStories(Default, { + args: { + external: true, + to: '/', + default: 'External link' + }, + parameters: { + docs: { + description: { + story: 'Forces target to be treated as an external link' + } + } + } +}) diff --git a/packages/frontend-2/components/common/text/Link.vue b/packages/frontend-2/components/common/text/Link.vue new file mode 100644 index 000000000..cae5785aa --- /dev/null +++ b/packages/frontend-2/components/common/text/Link.vue @@ -0,0 +1,82 @@ + + diff --git a/packages/frontend-2/components/common/tiptap/EmailMentionPopup.vue b/packages/frontend-2/components/common/tiptap/EmailMentionPopup.vue new file mode 100644 index 000000000..2217f8690 --- /dev/null +++ b/packages/frontend-2/components/common/tiptap/EmailMentionPopup.vue @@ -0,0 +1,55 @@ + + diff --git a/packages/frontend-2/components/common/tiptap/MentionList.vue b/packages/frontend-2/components/common/tiptap/MentionList.vue new file mode 100644 index 000000000..9fb4029ab --- /dev/null +++ b/packages/frontend-2/components/common/tiptap/MentionList.vue @@ -0,0 +1,88 @@ + + diff --git a/packages/frontend-2/components/common/tiptap/MentionListItem.vue b/packages/frontend-2/components/common/tiptap/MentionListItem.vue new file mode 100644 index 000000000..6287d5d9d --- /dev/null +++ b/packages/frontend-2/components/common/tiptap/MentionListItem.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/frontend-2/components/common/tiptap/TextEditor.vue b/packages/frontend-2/components/common/tiptap/TextEditor.vue new file mode 100644 index 000000000..8405ba711 --- /dev/null +++ b/packages/frontend-2/components/common/tiptap/TextEditor.vue @@ -0,0 +1,188 @@ + + + diff --git a/packages/frontend-2/components/connectors/Card.vue b/packages/frontend-2/components/connectors/Card.vue new file mode 100644 index 000000000..2c4efa686 --- /dev/null +++ b/packages/frontend-2/components/connectors/Card.vue @@ -0,0 +1,74 @@ + + diff --git a/packages/frontend-2/components/connectors/VersionsDownloadDialog.vue b/packages/frontend-2/components/connectors/VersionsDownloadDialog.vue new file mode 100644 index 000000000..704f28eff --- /dev/null +++ b/packages/frontend-2/components/connectors/VersionsDownloadDialog.vue @@ -0,0 +1,122 @@ + + diff --git a/packages/frontend-2/components/error/page/ProjectInviteBanner.vue b/packages/frontend-2/components/error/page/ProjectInviteBanner.vue new file mode 100644 index 000000000..c60089765 --- /dev/null +++ b/packages/frontend-2/components/error/page/ProjectInviteBanner.vue @@ -0,0 +1,43 @@ + + diff --git a/packages/frontend-2/components/form/Button.stories.ts b/packages/frontend-2/components/form/Button.stories.ts new file mode 100644 index 000000000..9bfb9587d --- /dev/null +++ b/packages/frontend-2/components/form/Button.stories.ts @@ -0,0 +1,265 @@ +import { userEvent, within } from '@storybook/testing-library' +import FormButton from '~~/components/form/Button.vue' +import { StoryObj, Meta } from '@storybook/vue3' +import { wait } from '@speckle/shared' +import { VuePlayFunction, mergeStories } from '~~/lib/common/helpers/storybook' +import { XMarkIcon } from '@heroicons/vue/24/solid' + +export default { + component: FormButton, + argTypes: { + color: { + options: ['default', 'invert', 'danger', 'warning', 'secondary'], + control: { type: 'select' } + }, + outlined: { + type: 'boolean' + }, + rounded: { + type: 'boolean' + }, + to: { + type: 'string' + }, + default: { + type: 'string', + description: 'Default slot holds button contents' + }, + click: { + action: 'click', + type: 'function' + }, + size: { + options: ['xs', 'sm', 'base', 'lg', 'xl'], + control: { type: 'select' } + }, + fullWidth: { + type: 'boolean' + }, + external: { + type: 'boolean' + }, + disabled: { + type: 'boolean' + }, + submit: { + type: 'boolean' + }, + iconLeft: { + type: 'function' + }, + iconRight: { + type: 'function' + } + }, + parameters: { + docs: { + description: { + component: 'A standard button to be used anywhere you need any kind of button' + } + } + } +} as Meta + +const clickPlayBuilder: (rightClick: boolean) => VuePlayFunction = + (rightClick) => + async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + + userEvent.click(button, rightClick ? { button: 2 } : undefined) + + await wait(1000) + + userEvent.tab() + } +const rightClickPlay = clickPlayBuilder(true) +const leftClickPlay = clickPlayBuilder(false) + +export const Default: StoryObj = { + render: (args) => ({ + components: { FormButton }, + setup() { + return { args } + }, + template: `{{ args.default || 'Submit' }}` + }), + play: rightClickPlay, + args: { + target: '_blank', + to: 'https://google.com', + default: 'Button text', + size: 'base', + type: 'standard', + fullWidth: false, + outlined: false, + rounded: false, + text: false, + link: false, + color: 'default', + disabled: false, + submit: false, + hideText: false + }, + parameters: { + docs: { + source: { + code: 'Hello World!' + } + } + } +} + +export const Rounded: StoryObj = mergeStories(Default, { + args: { + rounded: true + } +}) + +export const WarningButton: StoryObj = mergeStories(Default, { + args: { + color: 'warning' + } +}) + +export const RoundedOutlined: StoryObj = mergeStories(Default, { + args: { + rounded: true, + outlined: true + } +}) + +export const Outline: StoryObj = mergeStories(Default, { + args: { + outlined: true + } +}) + +export const Link: StoryObj = mergeStories(Default, { + args: { + link: true + }, + parameters: { + docs: { + description: { + story: 'Basically just a link (CommonTextLink is an alias for this)' + } + } + } +}) + +export const Text: StoryObj = mergeStories(Default, { + args: { + text: true + } +}) + +export const Small: StoryObj = mergeStories(Default, { + args: { + size: 'sm' + } +}) + +export const ExtraSmall: StoryObj = mergeStories(Default, { + args: { + size: 'xs' + } +}) + +export const Large: StoryObj = mergeStories(Default, { + args: { + size: 'lg' + } +}) + +export const ExtraLarge: StoryObj = mergeStories(Default, { + args: { + size: 'xl' + } +}) + +export const Disabled: StoryObj = mergeStories(Default, { + play: leftClickPlay, + args: { + disabled: true + }, + parameters: { + docs: { + description: { + story: 'Button is disabled and no mouse events fire' + } + } + } +}) + +export const NoTarget: StoryObj = mergeStories(Default, { + play: leftClickPlay, + args: { + to: null, + default: 'No URL, only for tracking click events' + } +}) + +export const FullWidth: StoryObj = mergeStories(Default, { + args: { + fullWidth: true, + default: 'Full width button' + } +}) + +export const External: StoryObj = mergeStories(Default, { + args: { + external: true, + to: '/', + default: 'External link' + }, + parameters: { + docs: { + description: { + story: 'Forces target to be treated as an external link' + } + } + } +}) + +export const Submit: StoryObj = mergeStories(Default, { + play: leftClickPlay, + args: { + to: null, + submit: true, + default: 'Submit button' + }, + parameters: { + docs: { + description: { + story: 'Rendered as button w/ type=submit, which will submit any parent forms' + } + } + } +}) + +export const LeftIcon: StoryObj = mergeStories(Default, { + args: { + iconLeft: XMarkIcon + } +}) + +export const RightIcon: StoryObj = mergeStories(Default, { + args: { + iconRight: XMarkIcon + } +}) + +export const IconOnBothSides: StoryObj = mergeStories(Default, { + args: { + iconRight: XMarkIcon, + iconLeft: XMarkIcon + } +}) + +export const IconOnly: StoryObj = mergeStories(Default, { + args: { + iconLeft: XMarkIcon, + hideText: true + } +}) diff --git a/packages/frontend-2/components/form/Button.vue b/packages/frontend-2/components/form/Button.vue new file mode 100644 index 000000000..87859e2ae --- /dev/null +++ b/packages/frontend-2/components/form/Button.vue @@ -0,0 +1,441 @@ + + + diff --git a/packages/frontend-2/components/form/CardButton.stories.ts b/packages/frontend-2/components/form/CardButton.stories.ts new file mode 100644 index 000000000..adf9198f1 --- /dev/null +++ b/packages/frontend-2/components/form/CardButton.stories.ts @@ -0,0 +1,87 @@ +import { userEvent, within } from '@storybook/testing-library' +import { Meta, StoryObj } from '@storybook/vue3' +import { wait } from '@speckle/shared' +import FormCardButton from '~~/components/form/CardButton.vue' +import { VuePlayFunction } from '~~/lib/common/helpers/storybook' + +type StoryType = StoryObj< + Record & { + 'update:modelValue': (val: boolean) => void + } +> + +export default { + component: FormCardButton, + argTypes: { + default: { + type: 'string', + description: 'Default slot holds button contents' + }, + click: { + action: 'click', + type: 'function' + }, + 'update:modelValue': { + action: 'update:modelValue', + type: 'function' + } + }, + parameters: { + docs: { + description: { + component: 'A card button that supports a toggled/selected state' + } + } + } +} as Meta + +const clickPlayBuilder: (rightClick?: boolean) => VuePlayFunction = + (rightClick) => + async ({ canvasElement }) => { + const canvas = within(canvasElement) + + userEvent.click(canvas.getByRole('button'), rightClick ? { button: 2 } : undefined) + + await wait(500) + + userEvent.click(canvas.getByRole('button'), rightClick ? { button: 2 } : undefined) + + userEvent.tab() + } + +export const Default: StoryType = { + render: (args, ctx) => ({ + components: { FormCardButton }, + setup() { + return { args } + }, + template: `{{ args.default || 'Text' }}`, + methods: { + onModelUpdate(val: boolean) { + args['update:modelValue'](val) + ctx.updateArgs({ ...args, modelValue: val }) + } + } + }), + play: clickPlayBuilder(), + args: { + default: 'Architecture', + disabled: false, + modelValue: false + } +} + +export const Disabled: StoryType = { + ...Default, + args: { + disabled: true + } +} + +export const Selected: StoryType = { + ...Default, + play: clickPlayBuilder(true), + args: { + modelValue: true + } +} diff --git a/packages/frontend-2/components/form/CardButton.vue b/packages/frontend-2/components/form/CardButton.vue new file mode 100644 index 000000000..a1f278380 --- /dev/null +++ b/packages/frontend-2/components/form/CardButton.vue @@ -0,0 +1,48 @@ + + diff --git a/packages/frontend-2/components/form/Checkbox.stories.ts b/packages/frontend-2/components/form/Checkbox.stories.ts new file mode 100644 index 000000000..291d945f6 --- /dev/null +++ b/packages/frontend-2/components/form/Checkbox.stories.ts @@ -0,0 +1,176 @@ +import FormCheckbox from '~~/components/form/Checkbox.vue' +import FormButton from '~~/components/form/Button.vue' +import { Meta, StoryObj } from '@storybook/vue3' +import { action } from '@storybook/addon-actions' +import { Form, SubmissionHandler } from 'vee-validate' +import { VuePlayFunction } from '~~/lib/common/helpers/storybook' +import { userEvent, within } from '@storybook/testing-library' +import { wait } from '@speckle/shared' +import { expect } from '@storybook/jest' + +export default { + component: FormCheckbox, + parameters: { + docs: { + description: { + component: + 'A checkbox, integrated with vee-validate for validation. Feed in rules through the `rules` prop. A checkbox can exist on its own or as part of a group. Checkboxes are grouped if they have the same name and have a parent form. The value structure differs between grouped and ungrouped checkboxes. If a checkbox is grouped, its v-model value will be an array of all values of all checked checkboxes in the group. Otherwise, its v-model value will either be its value if its checked or undefined if it isnt.' + } + } + }, + argTypes: { + value: { + type: 'string' + }, + rules: { + type: 'function' + }, + 'update:modelValue': { + type: 'function', + action: 'v-model' + }, + id: { + type: 'string' + } + } +} as Meta + +const toggleCheckboxPlayFunction: VuePlayFunction = async (params) => { + const canvas = within(params.canvasElement) + const checkbox = canvas.getByRole('checkbox') + + userEvent.click(checkbox) + await wait(1000) + userEvent.click(checkbox) +} + +export const Default: StoryObj = { + play: toggleCheckboxPlayFunction, + render: (args) => ({ + components: { FormCheckbox }, + setup() { + const vModelAction = action('v-model') + + return { args, vModelAction } + }, + template: `` + }), + parameters: { + docs: { + source: { + code: `` + } + } + }, + args: { + name: 'test1', + label: 'Example Checkbox', + description: 'Some help description here', + showRequired: false, + validateOnMount: false, + inlineDescription: false, + value: 'test1', + disabled: false + } +} + +export const Group: StoryObj = { + render: (args) => ({ + components: { FormCheckbox, Form, FormButton }, + setup() { + const fooModelValueHandler = action('foo@update:modelValue') + const barModelValueHandler = action('bar@update:modelValue') + const onSubmit: SubmissionHandler = (values) => action('onSubmit')(values) + + return { fooModelValueHandler, barModelValueHandler, onSubmit, args } + }, + template: ` +
+ + + Submit + + ` + }), + play: async (params) => { + const smallDelay = 500 + const bigDelay = 1000 + + const canvas = within(params.canvasElement) + + const fooCheckbox = canvas.getByAltText('foo') + const barCheckbox = canvas.getByAltText('bar') + const button = canvas.getByRole('button') + + userEvent.click(fooCheckbox) + await wait(smallDelay) + userEvent.click(button) + + await wait(bigDelay) + + userEvent.click(barCheckbox) + await wait(smallDelay) + userEvent.click(button) + + await wait(bigDelay) + + userEvent.click(fooCheckbox) + await wait(smallDelay) + userEvent.click(barCheckbox) + await wait(smallDelay) + userEvent.click(button) + }, + parameters: { + docs: { + description: { + story: + 'Checkboxes with the same name are part of a group and on form submit the value will be an array of all selected values. Check actions of this story for an example!' + } + } + } +} + +export const InlineDescription: StoryObj = { + ...Default, + args: { + name: 'inline1', + value: 'inline1-value', + inlineDescription: true, + label: 'Example checkbox', + description: 'This is an inline description' + } +} + +export const Disabled: StoryObj = { + ...Default, + play: (params) => { + const canvas = within(params.canvasElement) + const checkbox = canvas.getByRole('checkbox') + + const isChecked = (checkbox as HTMLInputElement).checked + + // click and assert that checkbox checked state hasn't changed + userEvent.click(checkbox) + + const newIsChecked = (checkbox as HTMLInputElement).checked + expect(isChecked).toBe(newIsChecked) + }, + args: { + name: 'disabled1', + value: 'disabled1-value', + label: 'Disabled checkbox', + disabled: true + } +} + +export const Required: StoryObj = { + ...Default, + args: { + name: 'required1', + value: 'required1-value', + label: 'Required checkbox', + showRequired: true, + rules: [(val: string | string[]) => (val ? true : 'This field is required')], + validateOnMount: true + } +} diff --git a/packages/frontend-2/components/form/Checkbox.vue b/packages/frontend-2/components/form/Checkbox.vue new file mode 100644 index 000000000..995ab19d2 --- /dev/null +++ b/packages/frontend-2/components/form/Checkbox.vue @@ -0,0 +1,194 @@ + + + diff --git a/packages/frontend-2/components/form/TextArea.stories.ts b/packages/frontend-2/components/form/TextArea.stories.ts new file mode 100644 index 000000000..9503dee53 --- /dev/null +++ b/packages/frontend-2/components/form/TextArea.stories.ts @@ -0,0 +1,115 @@ +import { userEvent, within } from '@storybook/testing-library' +import FormTextArea from '~~/components/form/TextArea.vue' +import { StoryObj, Meta } from '@storybook/vue3' +import { VuePlayFunction, mergeStories } from '~~/lib/common/helpers/storybook' +import { wait } from '@speckle/shared' + +export default { + component: FormTextArea, + parameters: { + docs: { + description: { + component: + 'A textarea box, integrated with vee-validate for validation. Feed in rules through the `rules` prop.' + } + } + }, + argTypes: { + rules: { + type: 'function' + }, + 'update:modelValue': { + type: 'function', + action: 'v-model' + } + } +} as Meta + +const generateRandomName = (prefix: string) => `${prefix}-${Math.random() * 100000}` + +const buildTextWriterPlayFunction = + (text: string): VuePlayFunction => + async (params) => { + const { canvasElement, viewMode } = params + const canvas = within(canvasElement) + const input = canvas.getByRole('textbox') + + // https://github.com/storybookjs/storybook/pull/19659 + await userEvent.type(input, text, { delay: viewMode === 'story' ? 100 : 0 }) + + userEvent.tab() + } + +export const Default: StoryObj = { + render: (args) => ({ + components: { FormTextArea }, + setup() { + return { args } + }, + template: `
+ +
` + }), + play: buildTextWriterPlayFunction('Hello world!'), + args: { + modelValue: '', + name: generateRandomName('default'), + help: 'Some help text', + placeholder: 'Placeholder text!', + label: 'Textarea w/ Label:', + showRequired: false, + showLabel: true, + disabled: false, + validateOnMount: false + }, + parameters: { + docs: { + source: { + code: `` + } + } + } +} + +export const Required = mergeStories(Default, { + play: async (params) => { + const { canvasElement } = params + await buildTextWriterPlayFunction('some text')(params) + + await wait(1000) + + const canvas = within(canvasElement) + const input = canvas.getByRole('textbox') + + userEvent.clear(input) + + await wait(1000) + + userEvent.tab() + }, + args: { + name: generateRandomName('required'), + label: 'This one is required!', + showRequired: true, + rules: [(val: string) => (val ? true : 'This field is required')], + validateOnMount: true + } +}) + +export const Disabled = mergeStories(Default, { + play: buildTextWriterPlayFunction('12345'), + args: { + name: generateRandomName('disabled'), + label: 'Disabled input', + disabled: true + } +}) + +export const WithClear = mergeStories(Default, { + play: buildTextWriterPlayFunction('12345'), + args: { + name: generateRandomName('withclear'), + label: 'Click on cross to clear', + showClear: true + } +}) diff --git a/packages/frontend-2/components/form/TextArea.vue b/packages/frontend-2/components/form/TextArea.vue new file mode 100644 index 000000000..030ebd258 --- /dev/null +++ b/packages/frontend-2/components/form/TextArea.vue @@ -0,0 +1,129 @@ +