diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 370e4140..00000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@vue/babel-preset-jsx"] -} diff --git a/.eslintrc.yml b/.eslintrc.yml index 5a3bcb8e..9836b8ca 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -8,7 +8,8 @@ rules: - error - 4 space-before-function-paren: 0 - no-callback-literal: 0 + no-var: 0 + array-callback-return: 0 globals: # Basic global 3rd party stuff _: readonly diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..a845b850 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1 @@ +npm run lint \ No newline at end of file diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 1cf23b4f..6fbd940b 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -1,34 +1,21 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Build storybook +name: Build and publish storybook on: push: branches: - - master - - feature/* + - "main" + - "feature/vue3" - pull_request: - branches: - - master - - feature/* +permissions: + contents: read + pages: write + id-token: write jobs: - build: + deploy: runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npx build-storybook - env: - CI: true + - id: build-publish + uses: bitovi/github-actions-storybook-to-github-pages@v1.0.2 + with: + path: storybook-static diff --git a/.storybook/assets/concrete/images/countries/jp.png b/.storybook/assets/concrete/images/countries/jp.png new file mode 100644 index 00000000..2d95f867 Binary files /dev/null and b/.storybook/assets/concrete/images/countries/jp.png differ diff --git a/.storybook/assets/concrete/images/countries/us.png b/.storybook/assets/concrete/images/countries/us.png new file mode 100644 index 00000000..e1ebe006 Binary files /dev/null and b/.storybook/assets/concrete/images/countries/us.png differ diff --git a/.storybook/public/concrete/images/icons/bedrock/sprites.svg b/.storybook/assets/concrete/images/icons/bedrock/sprites.svg similarity index 100% rename from .storybook/public/concrete/images/icons/bedrock/sprites.svg rename to .storybook/assets/concrete/images/icons/bedrock/sprites.svg diff --git a/.storybook/assets/concrete/preview.html b/.storybook/assets/concrete/preview.html new file mode 100644 index 00000000..dcdba650 --- /dev/null +++ b/.storybook/assets/concrete/preview.html @@ -0,0 +1,3 @@ +

+Example preview +

\ No newline at end of file diff --git a/.storybook/assets/images/examplejpeg.jpeg b/.storybook/assets/images/examplejpeg.jpeg new file mode 100644 index 00000000..c1813afe Binary files /dev/null and b/.storybook/assets/images/examplejpeg.jpeg differ diff --git a/.storybook/assets/mockServiceWorker.js b/.storybook/assets/mockServiceWorker.js new file mode 100644 index 00000000..51d85eee --- /dev/null +++ b/.storybook/assets/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.3.2). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/.storybook/assets/scss/theme/main.scss b/.storybook/assets/scss/theme/main.scss index c59f29a3..fcbc1bea 100644 --- a/.storybook/assets/scss/theme/main.scss +++ b/.storybook/assets/scss/theme/main.scss @@ -1,71 +1,2 @@ -// 01. Bootstrap Functions go first. -@import "node_modules/bootstrap/scss/functions"; - -// 02. Bootstrap overrides, have to be done BEFORE bootstrap. -// @todo this is all busted up, update it for new bedrock. -//@import "../../../../assets/cms/scss/bootstrap-overrides"; - -// 03. Bootstrap Variables -@import "node_modules/bootstrap/scss/variables"; - -// 04. Theme variables, some of which can now be based on bootstrap variable/constants/ -@import "../../../../assets/cms/scss/variables"; - -// Import the bootstrap mixins, variables and functions -@import "node_modules/bootstrap/scss/mixins"; - -// Import the base classes like core utilities and variables -@import "node_modules/@fortawesome/fontawesome-free/scss/variables"; - -// Import the subset of Bootstrap that is required in the CMS entry point. -// Make sure to namespace it to .ccm-ui -// Reboot HTML body and reboot tags are basically the bootstrap reboot.scss file split into two parts. -// The html and body tags are combined into the reboot_html_body.scss file, and the rest of the tags are -// just copied into reboot_tags.scs -@import "../../../../assets/cms/scss/bootstrap/reboot"; -div.ccm-ui { - @import "../../../../assets/cms/scss/bootstrap/reboot-tags"; - - // Import the bootstrap components we want. Note: I'm including everything in the list, and commenting out - // things that are either a) already included, b) not applicable, c) not used in our in-page CMS sub-set. - //@import "functions"; - //@import "variables"; - //@import "mixins"; - //@import "root"; - //@import "reboot"; - @import "node_modules/bootstrap/scss/type"; - //@import "images"; - @import "node_modules/bootstrap/scss/code"; - @import "node_modules/bootstrap/scss/grid"; - @import "node_modules/bootstrap/scss/tables"; - @import "node_modules/bootstrap/scss/forms"; - @import "node_modules/bootstrap/scss/buttons"; - @import "node_modules/bootstrap/scss/transitions"; - @import "node_modules/bootstrap/scss/dropdown"; - @import "node_modules/bootstrap/scss/button-group"; - @import "node_modules/bootstrap/scss/input-group"; - @import "node_modules/bootstrap/scss/custom-forms"; - @import "node_modules/bootstrap/scss/nav"; - //@import "navbar"; - @import "node_modules/bootstrap/scss/card"; - @import "node_modules/bootstrap/scss/breadcrumb"; - @import "node_modules/bootstrap/scss/pagination"; - @import "node_modules/bootstrap/scss/badge"; - //@import "jumbotron"; - @import "node_modules/bootstrap/scss/alert"; - @import "node_modules/bootstrap/scss/progress"; - @import "node_modules/bootstrap/scss/media"; - @import "node_modules/bootstrap/scss/list-group"; - @import "node_modules/bootstrap/scss/close"; - @import "node_modules/bootstrap/scss/toasts"; - //@import "modal"; - @import "node_modules/bootstrap/scss/tooltip"; - @import "node_modules/bootstrap/scss/popover"; - //@import "carousel"; - //@import "spinners"; - @import "node_modules/bootstrap/scss/utilities"; - //@import "print"; -} - -// This is the actual public entry point to the cms.css on the front-end -@import "../../../../assets/cms/scss/base"; \ No newline at end of file +@import '../../../../assets/bedrock/scss/frontend'; +@import '../../../../assets/account/scss/frontend'; \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index 6023208d..67454c26 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,19 +1,21 @@ -const path = require('path'); - -// Load in laravel mix -const custom = require('../node_modules/laravel-mix/setup/webpack.config.js') - -module.exports = { - stories: ['../stories/**/*.stories.[tj]s'], - webpackFinal: config => { - return { - ...config, - module: { - ...config.module, - rules: [ - ...custom.module.rules - ] - } - } - } -} \ No newline at end of file +/** @type { import('@storybook/vue3-vite').StorybookConfig } */ +export default { + stories: [ + "../stories/Docs.mdx", + "../stories/**/*.mdx", + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", + ], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/vue3-vite", + options: {}, + }, + docs: { + autodocs: "tag", + }, + staticDirs: [{from: './assets', 'to': '/'}] +}; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html deleted file mode 100644 index 431162a1..00000000 --- a/.storybook/preview-head.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/.storybook/preview.js b/.storybook/preview.js index 8826134c..cfece269 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,2 +1,52 @@ +import { initialize, mswLoader } from 'msw-storybook-addon'; import '@fortawesome/fontawesome-free/css/all.css' import './assets/scss/theme/main.scss' + +// Bootstrap +import * as bootstrap from 'bootstrap/dist/js/bootstrap.esm'; + +// JQuery +import jQuery from "jquery"; + +window.$ = window.jquery = window.jQuery = jQuery +window.bootstrap = bootstrap + +$.fn.modal = (...args) => new bootstrap.Modal(...args) + +const mswOptions = { + onUnhandledRequest({method, url}) { + if (url.pathname.startsWith('/ccm')) { + console.error(`Unhandled ${method} request to ${url}`) + } + } +} + +if (window.location.host.slice(-10) === '.github.io') { + mswOptions.serviceWorker = { + url: '/' + window.location.pathname.split('/')[1] + '/mockServiceWorker.js' + } +} + +// Initialize MSW +initialize(mswOptions); + +/** @type { import('@storybook/vue3').Preview } */ +const preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + loaders: [mswLoader], + decorators: [ + () => ({ + template: `
`, + }) + ] +} + +export default preview; diff --git a/README.md b/README.md index 54909871..a13e0677 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,16 @@ Bedrock is Concrete's front end library. It is used to support Concrete itself, ## More Information https://documentation.concretecms.org/tutorials/submitting-user-interface-pull-requests-for-concrete-cms-version-9 + +## Storybook +[View our storybook](https://concretecms.github.io/bedrock/?path=/docs/docs--docs) for examples of how to use each +component + +See [/stories/ExampleStories.js](stories/ExampleStories.js) for an example for how to add stories and +[/stories/Docs.mdx](stories/Docs.mdx) to see how to add documentation pages. + +## Githooks +To run the provided [.githooks](.githooks) in your local repository, run: +```bash +git config --local core.hooksPath .githooks +``` \ No newline at end of file diff --git a/assets/account/js/frontend/components/AvatarCropper.vue b/assets/account/js/frontend/components/AvatarCropper.vue index afa925b2..12e1891f 100644 --- a/assets/account/js/frontend/components/AvatarCropper.vue +++ b/assets/account/js/frontend/components/AvatarCropper.vue @@ -27,7 +27,7 @@ {{ lang.saveInProgress }} - + - diff --git a/assets/cms/components/Announcement/Action/ExternalLinkAction.vue b/assets/cms/components/Announcement/Action/ExternalLinkAction.vue index 83833b60..1d19bcee 100644 --- a/assets/cms/components/Announcement/Action/ExternalLinkAction.vue +++ b/assets/cms/components/Announcement/Action/ExternalLinkAction.vue @@ -14,12 +14,6 @@ export default { type: String, required: true } - }, - data: () => ({ - }), - methods: { - }, - mounted() { } } diff --git a/assets/cms/components/Announcement/Action/GuideAction.vue b/assets/cms/components/Announcement/Action/GuideAction.vue index 49b95ab2..a6c914f3 100644 --- a/assets/cms/components/Announcement/Action/GuideAction.vue +++ b/assets/cms/components/Announcement/Action/GuideAction.vue @@ -1,6 +1,6 @@ diff --git a/assets/cms/components/Announcement/Header/Header.vue b/assets/cms/components/Announcement/Header/Header.vue index 89607823..154cb615 100644 --- a/assets/cms/components/Announcement/Header/Header.vue +++ b/assets/cms/components/Announcement/Header/Header.vue @@ -26,8 +26,6 @@ export default { type: String, required: true } - }, - data: () => ({ - }) + } } diff --git a/assets/cms/components/Announcement/Item/Item.vue b/assets/cms/components/Announcement/Item/Item.vue index 75c76817..a23f0da1 100644 --- a/assets/cms/components/Announcement/Item/Item.vue +++ b/assets/cms/components/Announcement/Item/Item.vue @@ -16,9 +16,9 @@ diff --git a/assets/cms/components/Announcement/Modal/Modal.vue b/assets/cms/components/Announcement/Modal/Modal.vue index 4a9933c1..b18f65aa 100644 --- a/assets/cms/components/Announcement/Modal/Modal.vue +++ b/assets/cms/components/Announcement/Modal/Modal.vue @@ -11,9 +11,9 @@ diff --git a/assets/cms/components/Icon.vue b/assets/cms/components/Icon.vue index 1f754268..9121a0e7 100644 --- a/assets/cms/components/Icon.vue +++ b/assets/cms/components/Icon.vue @@ -1,20 +1,19 @@ - + diff --git a/assets/cms/components/Pagination.vue b/assets/cms/components/Pagination.vue index 61053277..b2184892 100644 --- a/assets/cms/components/Pagination.vue +++ b/assets/cms/components/Pagination.vue @@ -1,5 +1,5 @@ diff --git a/assets/cms/components/express/Selector.vue b/assets/cms/components/express/Selector.vue index 06a8c2a1..b3522b86 100644 --- a/assets/cms/components/express/Selector.vue +++ b/assets/cms/components/express/Selector.vue @@ -80,8 +80,8 @@ diff --git a/assets/cms/components/form/PasswordInput.vue b/assets/cms/components/form/PasswordInput.vue index f6de3e3c..728a2d11 100644 --- a/assets/cms/components/form/PasswordInput.vue +++ b/assets/cms/components/form/PasswordInput.vue @@ -66,6 +66,7 @@ export default { default: false } }, + emits: ['change'], computed: { inputType() { return this.passwordVisible ? 'text' : 'password' @@ -88,6 +89,11 @@ export default { ) } }, + watch: { + enteredPassword(v) { + this.$emit('change', v) + } + }, mounted() { if (window.ccmi18n_passwordInput) { for (const key in this.i18n) { diff --git a/assets/cms/components/form/Toggle.vue b/assets/cms/components/form/Toggle.vue index 4031c9e1..79f00f19 100644 --- a/assets/cms/components/form/Toggle.vue +++ b/assets/cms/components/form/Toggle.vue @@ -1,15 +1,15 @@ -