From a3afc03163ea40fd4e7d529115a08b8c0cd63bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Gr=C3=B8ngaard?= Date: Fri, 7 Jun 2024 11:50:05 +0200 Subject: [PATCH] feat: add new @sanity/insert-menu package (#1630) * feat: add new @sanity/insert-menu package In order to expose this menu to both `@sanity/visual-editing` and `sanity`. This repository might not be the best permanent resident for this package, but that's OK for now. * fixup! feat: add new @sanity/insert-menu package * fixup! feat: add new @sanity/insert-menu package --- packages/insert-menu/.editorconfig | 9 + packages/insert-menu/.eslintignore | 1 + packages/insert-menu/.eslintrc.cjs | 53 ++ packages/insert-menu/.gitignore | 8 + packages/insert-menu/README.md | 1 + packages/insert-menu/package.config.ts | 7 + packages/insert-menu/package.json | 66 ++ packages/insert-menu/src/InsertMenu.tsx | 297 +++++++ packages/insert-menu/src/InsertMenuOptions.ts | 10 + packages/insert-menu/src/getSchemaTypeIcon.ts | 21 + packages/insert-menu/src/index.ts | 2 + packages/insert-menu/tsconfig.dist.json | 8 + packages/insert-menu/tsconfig.json | 5 + packages/insert-menu/tsconfig.settings.json | 7 + pnpm-lock.yaml | 733 +++++++++++++++++- release-please-config.json | 1 + 16 files changed, 1198 insertions(+), 31 deletions(-) create mode 100644 packages/insert-menu/.editorconfig create mode 100644 packages/insert-menu/.eslintignore create mode 100644 packages/insert-menu/.eslintrc.cjs create mode 100644 packages/insert-menu/.gitignore create mode 100644 packages/insert-menu/README.md create mode 100644 packages/insert-menu/package.config.ts create mode 100644 packages/insert-menu/package.json create mode 100644 packages/insert-menu/src/InsertMenu.tsx create mode 100644 packages/insert-menu/src/InsertMenuOptions.ts create mode 100644 packages/insert-menu/src/getSchemaTypeIcon.ts create mode 100644 packages/insert-menu/src/index.ts create mode 100644 packages/insert-menu/tsconfig.dist.json create mode 100644 packages/insert-menu/tsconfig.json create mode 100644 packages/insert-menu/tsconfig.settings.json diff --git a/packages/insert-menu/.editorconfig b/packages/insert-menu/.editorconfig new file mode 100644 index 000000000..9d08a1a82 --- /dev/null +++ b/packages/insert-menu/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/packages/insert-menu/.eslintignore b/packages/insert-menu/.eslintignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/packages/insert-menu/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/packages/insert-menu/.eslintrc.cjs b/packages/insert-menu/.eslintrc.cjs new file mode 100644 index 000000000..595e7970b --- /dev/null +++ b/packages/insert-menu/.eslintrc.cjs @@ -0,0 +1,53 @@ +'use strict' + +/** @type import('eslint').Linter.Config */ +module.exports = { + root: true, + env: { + browser: true, + es6: true, + node: true, + }, + extends: ['eslint:recommended'], + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + plugins: ['import', 'simple-import-sort'], + rules: { + 'no-console': 'error', + 'no-shadow': 'error', + 'no-warning-comments': [ + 'warn', + { + location: 'start', + terms: ['todo', 'fixme'], + }, + ], + 'quote-props': ['warn', 'consistent-as-needed'], + 'simple-import-sort/exports': 'warn', + 'simple-import-sort/imports': 'warn', + 'strict': ['warn', 'global'], + }, + overrides: [ + { + files: ['**/*.ts', '**/*.tsx'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: ['./tsconfig.json'], + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + plugins: ['import', '@typescript-eslint', 'simple-import-sort'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': 'error', + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/member-delimiter-style': 'off', + '@typescript-eslint/no-empty-interface': 'off', + }, + }, + ], +} diff --git a/packages/insert-menu/.gitignore b/packages/insert-menu/.gitignore new file mode 100644 index 000000000..4a76ee26f --- /dev/null +++ b/packages/insert-menu/.gitignore @@ -0,0 +1,8 @@ +*.local +*.log +*.tgz + +.DS_Store +dist +etc +node_modules diff --git a/packages/insert-menu/README.md b/packages/insert-menu/README.md new file mode 100644 index 000000000..578a677d4 --- /dev/null +++ b/packages/insert-menu/README.md @@ -0,0 +1 @@ +# `@sanity/insert-menu` diff --git a/packages/insert-menu/package.config.ts b/packages/insert-menu/package.config.ts new file mode 100644 index 000000000..d0101a35d --- /dev/null +++ b/packages/insert-menu/package.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from '@sanity/pkg-utils' + +// https://github.com/sanity-io/pkg-utils#configuration +export default defineConfig({ + // the path to the tsconfig file for distributed builds + tsconfig: 'tsconfig.dist.json', +}) diff --git a/packages/insert-menu/package.json b/packages/insert-menu/package.json new file mode 100644 index 000000000..311c27bd2 --- /dev/null +++ b/packages/insert-menu/package.json @@ -0,0 +1,66 @@ +{ + "name": "@sanity/insert-menu", + "version": "0.0.0", + "description": "", + "keywords": [], + "homepage": "https://github.com/sanity-io/visual-editing/tree/main/packages/insert-menu#readme", + "bugs": { + "url": "https://github.com/sanity-io/visual-editing/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/sanity-io/visual-editing.git", + "directory": "packages/insert-menu" + }, + "license": "MIT", + "author": "Sanity.io ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "source": "./src/index.ts", + "require": "./dist/index.cjs", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "pkg build --strict --clean --check", + "lint": "eslint .", + "ts:check": "tsc --noEmit" + }, + "browserslist": "extends @sanity/browserslist-config", + "dependencies": { + "@sanity/icons": "^3.0.0", + "@sanity/ui": "^2.3.0" + }, + "devDependencies": { + "@sanity/pkg-utils": "^6.9.1", + "@typescript-eslint/eslint-plugin": "^7.12.0", + "@typescript-eslint/parser": "^7.12.0", + "eslint": "^8.57.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-simple-import-sort": "^12.1.0", + "lint-staged": "^15.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-is": "^18.3.1", + "sanity": "3.45.0", + "typescript": "^5.4.5" + }, + "peerDependencies": { + "react": "^18.3 || >=19.0.0-rc", + "react-dom": "^18.3 || >=19.0.0-rc", + "react-is": "^18.3 || >=19.0.0-rc" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/insert-menu/src/InsertMenu.tsx b/packages/insert-menu/src/InsertMenu.tsx new file mode 100644 index 000000000..e67509811 --- /dev/null +++ b/packages/insert-menu/src/InsertMenu.tsx @@ -0,0 +1,297 @@ +import {SearchIcon, ThLargeIcon, UlistIcon} from '@sanity/icons' +import {type SchemaType} from '@sanity/types' +import { + Box, + Button, + Flex, + Grid, + Menu, + MenuItem, + type MenuItemProps, + Stack, + Tab, + TabList, + Text, + TextInput, + Tooltip, +} from '@sanity/ui' +import {type ChangeEvent, createElement, useReducer, useState} from 'react' +import {isValidElementType} from 'react-is' + +import {getSchemaTypeIcon} from './getSchemaTypeIcon' +import type {InsertMenuOptions} from './InsertMenuOptions' + +type InsertMenuGroup = NonNullable[number] & {selected: boolean} +type InsertMenuViews = NonNullable +type InsertMenuView = InsertMenuViews[number] + +type InsertMenuEvent = + | {type: 'toggle view'; name: InsertMenuView['name']} + | {type: 'change query'; query: string} + | {type: 'select group'; name: string | undefined} + +type InsertMenuState = { + query: string + groups: Array + views: Array +} + +function fullInsertMenuReducer(state: InsertMenuState, event: InsertMenuEvent): InsertMenuState { + return { + query: event.type === 'change query' ? event.query : state.query, + groups: + event.type === 'select group' + ? state.groups.map((group) => ({...group, selected: event.name === group.name})) + : state.groups, + views: + event.type === 'toggle view' + ? state.views.map((view) => ({...view, selected: event.name === view.name})) + : state.views, + } +} + +const ALL_ITEMS_GROUP_NAME = 'all-items' + +/** @alpha */ +export type InsertMenuProps = InsertMenuOptions & { + schemaTypes: Array + onSelect: (schemaType: SchemaType) => void + labels: { + 'insert-menu.filter.all-items': string + 'insert-menu.search.no-results': string + 'insert-menu.search.placeholder': string + 'insert-menu.toggle-grid-view.tooltip': string + 'insert-menu.toggle-list-view.tooltip': string + } +} + +/** @alpha */ +export function InsertMenu(props: InsertMenuProps): React.JSX.Element { + const showIcons = props.icons === undefined ? true : props.icons + const [state, send] = useReducer(fullInsertMenuReducer, { + query: '', + groups: props.groups + ? [ + { + name: ALL_ITEMS_GROUP_NAME, + title: props.labels['insert-menu.filter.all-items'], + selected: true, + }, + ...props.groups.map((group) => ({...group, selected: false})), + ] + : [], + views: (props.views ?? [{name: 'list'}]).map((view, index) => ({ + ...view, + selected: index === 0, + })), + }) + const filteredSchemaTypes = filterSchemaTypes(props.schemaTypes, state.query, state.groups) + const selectedView = state.views.find((view) => view.selected) + + return ( + + + + {props.filter ? ( + + ) => { + send({type: 'change query', query: event.target.value}) + }} + placeholder={props.labels['insert-menu.search.placeholder']} + value={state.query} + /> + + ) : null} + {state.views.length > 1 ? ( + + { + send({type: 'toggle view', name}) + }} + labels={props.labels} + /> + + ) : null} + + + {state.groups && state.groups.length > 0 ? ( + + {state.groups.map((group) => ( + { + send({type: 'select group', name: group.name}) + }} + /> + ))} + + ) : null} + {filteredSchemaTypes.length === 0 ? ( + + + {props.labels['insert-menu.search.no-results']} + + + ) : !selectedView ? null : selectedView.name === 'grid' ? ( + + {filteredSchemaTypes.map((schemaType) => ( + { + props.onSelect(schemaType) + }} + previewUrl={selectedView.previewUrl} + schemaType={schemaType} + /> + ))} + + ) : ( + + {filteredSchemaTypes.map((schemaType) => ( + { + props.onSelect(schemaType) + }} + text={schemaType.title ?? schemaType.name} + /> + ))} + + )} + + + + ) +} + +const viewToggleIcon: Record = { + grid: ThLargeIcon, + list: UlistIcon, +} + +const viewToggleTooltip: Record = { + grid: 'insert-menu.toggle-grid-view.tooltip', + list: 'insert-menu.toggle-list-view.tooltip', +} + +type ViewToggleProps = { + views: InsertMenuState['views'] + onToggle: (viewName: InsertMenuView['name']) => void + labels: Pick< + InsertMenuProps['labels'], + 'insert-menu.toggle-grid-view.tooltip' | 'insert-menu.toggle-list-view.tooltip' + > +} + +function ViewToggle(props: ViewToggleProps) { + const viewIndex = props.views.findIndex((view) => view.selected) + const nextView = props.views[viewIndex + 1] ?? props.views[0] + + return ( + +