├── react ├── .eslintignore ├── .babelrc ├── components │ ├── consts.ts │ ├── EditorContainer │ │ ├── Sidebar │ │ │ ├── consts.ts │ │ │ ├── BlockSelector │ │ │ │ ├── BlockList │ │ │ │ │ ├── BlockListItem │ │ │ │ │ │ ├── Item.css │ │ │ │ │ │ └── ExpandArrow │ │ │ │ │ │ │ ├── styles.css │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── typings.d.ts │ │ │ ├── BlockEditor │ │ │ │ ├── BlockConfigurationEditor │ │ │ │ │ ├── ConditionControls │ │ │ │ │ │ ├── typings.d.ts │ │ │ │ │ │ ├── Separator.tsx │ │ │ │ │ │ ├── ConditionTitle.tsx │ │ │ │ │ │ └── ScopeSelector │ │ │ │ │ │ │ └── ScopeSelector.tsx │ │ │ │ │ ├── typings.d.ts │ │ │ │ │ ├── styles.css │ │ │ │ │ └── hooks.ts │ │ │ │ ├── BlockConfigurationList │ │ │ │ │ ├── Card │ │ │ │ │ │ ├── typings.d.ts │ │ │ │ │ │ ├── utils.ts │ │ │ │ │ │ ├── styles.css │ │ │ │ │ │ ├── ConditionTags │ │ │ │ │ │ │ └── Tag.tsx │ │ │ │ │ │ └── StatusLabel.tsx │ │ │ │ │ ├── CreateButton.tsx │ │ │ │ │ └── typings.d.ts │ │ │ │ ├── utils.test.ts │ │ │ │ └── utils.ts │ │ │ ├── Transitions │ │ │ │ ├── consts.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── Enter │ │ │ │ │ ├── styles.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── typings.d.ts │ │ │ │ └── Exit │ │ │ │ │ ├── styles.css │ │ │ │ │ └── index.tsx │ │ │ ├── AbsoluteLoader.tsx │ │ │ ├── utils.test.ts │ │ │ ├── FormMetaContext.tsx │ │ │ └── styles.css │ │ ├── Topbar │ │ │ ├── DeviceSwitcher │ │ │ │ ├── DeviceSwitcher.css │ │ │ │ ├── messages.ts │ │ │ │ ├── icons │ │ │ │ │ ├── IconMobile.tsx │ │ │ │ │ ├── IconTablet.tsx │ │ │ │ │ └── IconDesktop.tsx │ │ │ │ ├── consts.ts │ │ │ │ ├── DeviceItem.tsx │ │ │ │ └── index.tsx │ │ │ ├── ContextSelectors │ │ │ │ ├── graphql │ │ │ │ │ ├── appSettings.graphql │ │ │ │ │ ├── TenantInfo.graphql │ │ │ │ │ ├── CopyBindings.graphql │ │ │ │ │ ├── SaveRoute.graphql │ │ │ │ │ └── GetRoute.graphql │ │ │ │ ├── typings.d.ts │ │ │ │ ├── hooks │ │ │ │ │ └── useBinding.ts │ │ │ │ └── BindingCloning │ │ │ │ │ └── utils │ │ │ │ │ └── initialReducerState.ts │ │ │ ├── hooks.ts │ │ │ ├── icons │ │ │ │ ├── IconView.tsx │ │ │ │ ├── IconPicker.tsx │ │ │ │ └── CopyContent.tsx │ │ │ ├── index.tsx │ │ │ ├── SidebarVisibilityToggle.tsx │ │ │ └── BlockPicker.tsx │ │ ├── Styles │ │ │ ├── StyleEditor │ │ │ │ ├── graphql │ │ │ │ │ ├── DeleteFontFamily.graphql │ │ │ │ │ ├── GenerateStyleSheet.graphql │ │ │ │ │ ├── RenameStyle.graphql │ │ │ │ │ ├── UpdateStyle.graphql │ │ │ │ │ ├── ListFonts.graphql │ │ │ │ │ └── SaveFontFamily.graphql │ │ │ │ ├── colors.d.ts │ │ │ │ ├── queries │ │ │ │ │ ├── GenerateStyleSheet.ts │ │ │ │ │ └── ListFontsQuery.ts │ │ │ │ ├── typings.d.ts │ │ │ │ ├── mutations │ │ │ │ │ ├── RenameStyle.ts │ │ │ │ │ ├── UpdateStyle.ts │ │ │ │ │ └── DeleteFontFamily.ts │ │ │ │ ├── utils │ │ │ │ │ └── colors.ts │ │ │ │ ├── typography │ │ │ │ │ ├── TypeTokensEntry.tsx │ │ │ │ │ ├── TypeTokensList.tsx │ │ │ │ │ ├── TypographyEditor.tsx │ │ │ │ │ ├── FontFamilyEntry.tsx │ │ │ │ │ └── TypeTokenDropdown.tsx │ │ │ │ ├── AvailableEditor.tsx │ │ │ │ └── index.tsx │ │ │ ├── StyleList │ │ │ │ ├── graphql │ │ │ │ │ ├── DeleteStyle.graphql │ │ │ │ │ ├── SaveSelectedStyle.graphql │ │ │ │ │ └── CreateStyle.graphql │ │ │ │ ├── queries │ │ │ │ │ └── ListStyles.ts │ │ │ │ ├── mutations │ │ │ │ │ ├── DeleteStyle.ts │ │ │ │ │ ├── CreateStyle.ts │ │ │ │ │ └── SaveSelectedStyle.ts │ │ │ │ └── icons │ │ │ │ │ └── CreateNewIcon.tsx │ │ │ ├── typings │ │ │ │ ├── typings.d.ts │ │ │ │ └── config.d.ts │ │ │ └── components │ │ │ │ ├── Colors.tsx │ │ │ │ └── Typography.tsx │ │ ├── EditableText │ │ │ └── styles.css │ │ ├── graphql │ │ │ ├── DeleteContent.graphql │ │ │ ├── SendEventToAudit.graphql │ │ │ ├── ListContent.graphql │ │ │ └── SaveContent.graphql │ │ ├── EditorContainer.css │ │ ├── mutations │ │ │ ├── DeleteContent.tsx │ │ │ ├── SaveContent.tsx │ │ │ └── SendEventToAudit.tsx │ │ └── queries │ │ │ └── ListContent.tsx │ ├── admin │ │ ├── redirects │ │ │ ├── UploadModal │ │ │ │ ├── typings.d.ts │ │ │ │ ├── UploadPrompt │ │ │ │ │ ├── UploadPrompt.css │ │ │ │ │ └── validateRedirect.ts │ │ │ │ ├── UploadModal.css │ │ │ │ └── Loading.tsx │ │ │ ├── mutations │ │ │ │ ├── SaveRedirectFromFile.graphql │ │ │ │ ├── DeleteManyRedirectsFromFile.graphql │ │ │ │ ├── DeleteManyRedirectsFromFile.d.ts │ │ │ │ └── SaveRedirectFromFile.tsx │ │ │ ├── ImportErrorModal │ │ │ │ └── ImportErrorModal.css │ │ │ ├── List │ │ │ │ ├── typings.d.ts │ │ │ │ ├── CreateButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── typings.d.ts │ │ │ ├── consts.ts │ │ │ ├── Form │ │ │ │ ├── Operations.tsx │ │ │ │ └── typings.d.ts │ │ │ └── bulkUploadRedirects.ts │ │ ├── pages │ │ │ ├── List │ │ │ │ ├── utils.ts │ │ │ │ ├── SectionSeparator.tsx │ │ │ │ ├── typings.d.ts │ │ │ │ ├── Entry.tsx │ │ │ │ └── Section.tsx │ │ │ ├── SeparatorWithLine.tsx │ │ │ ├── Form │ │ │ │ ├── Title.tsx │ │ │ │ ├── stateHandlers │ │ │ │ │ ├── getLoginToggleState.ts │ │ │ │ │ ├── __fixtures__ │ │ │ │ │ │ ├── setupDate.ts │ │ │ │ │ │ ├── state.ts │ │ │ │ │ │ └── newPage.ts │ │ │ │ │ ├── getRemoveConditionalTemplateState.ts │ │ │ │ │ ├── getLoginToggleState.test.ts │ │ │ │ │ ├── getChangeTemplateConditionalTemplateState.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── getChangeStatementsConditionalTemplate.ts │ │ │ │ │ ├── getChangeOperatorConditionalTemplateState.ts │ │ │ │ │ └── getAddConditionalTemplateState.ts │ │ │ │ ├── SectionTitle.tsx │ │ │ │ ├── utils.test.ts │ │ │ │ └── typings.d.ts │ │ │ ├── consts.ts │ │ │ └── utils.ts │ │ ├── FormFieldSeparator.tsx │ │ ├── store │ │ │ ├── PWAForm │ │ │ │ ├── mutations │ │ │ │ │ ├── UpdatePWASettings.graphql │ │ │ │ │ ├── UpdateManifestIcon.graphql │ │ │ │ │ └── UpdateManifest.graphql │ │ │ │ └── queries │ │ │ │ │ └── PWA.graphql │ │ │ └── StoreForm │ │ │ │ ├── queries │ │ │ │ ├── InstalledApp.graphql │ │ │ │ └── AvailableApp.graphql │ │ │ │ └── mutations │ │ │ │ └── SaveAppSettings.graphql │ │ ├── institutional │ │ │ ├── Form │ │ │ │ ├── Loader.tsx │ │ │ │ ├── UnallowedWarning.tsx │ │ │ │ └── utils.tsx │ │ │ ├── utils.tsx │ │ │ └── List │ │ │ │ └── Entry.tsx │ │ ├── AdminStructure.tsx │ │ ├── TargetPathContext.tsx │ │ └── utils.ts │ ├── icons │ │ ├── DragHandle.css │ │ ├── ContentActiveIcon.tsx │ │ ├── ContentInactiveIcon.tsx │ │ ├── DangerIcon.tsx │ │ ├── ArrowIcon.tsx │ │ ├── ComponentDragHandleIcon.tsx │ │ ├── CalendarIcon.tsx │ │ ├── utils.tsx │ │ ├── PersonIcon.tsx │ │ ├── PageIcon.tsx │ │ ├── TemplateIcon.tsx │ │ ├── ContentScheduledIcon.tsx │ │ ├── AddIcon.tsx │ │ ├── EarthIcon.tsx │ │ ├── DragHandle.tsx │ │ └── GearIcon.tsx │ ├── ActionMenu │ │ ├── typings.d.ts │ │ └── index.tsx │ ├── form │ │ ├── ImageUploader │ │ │ ├── ImagePreview.css │ │ │ ├── styles.css │ │ │ ├── ErrorAlert.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── Dropzone.tsx │ │ │ └── ImagePreview.tsx │ │ ├── RichText.tsx │ │ ├── I18nInput │ │ │ └── utils.ts │ │ ├── ArrayFieldTemplate │ │ │ ├── ArrayFieldTemplateItem │ │ │ │ ├── PreviewOverlay.tsx │ │ │ │ ├── Handle.tsx │ │ │ │ ├── NoImagePlaceholder.tsx │ │ │ │ ├── ArrayFieldTemplateItem.css │ │ │ │ └── icons │ │ │ │ │ └── IconImage.tsx │ │ │ ├── AddButton.tsx │ │ │ ├── ItemTransitions.css │ │ │ └── styles.css │ │ ├── typings.d.ts │ │ ├── FieldTemplate.tsx │ │ ├── ObjectFieldTemplate.tsx │ │ ├── Toggle.tsx │ │ └── MultiSelect.tsx │ ├── Loader.tsx │ ├── RichTextEditor │ │ ├── Link.tsx │ │ ├── Media.tsx │ │ ├── style.css │ │ └── StyleButton.tsx │ ├── HighlightOverlay │ │ ├── OverlayMask │ │ │ ├── OverlayMask.css │ │ │ └── index.tsx │ │ ├── typings.d.ts │ │ ├── hooks │ │ │ ├── useStyles │ │ │ │ ├── typings.d.ts │ │ │ │ └── index.ts │ │ │ └── useAutoScroll.ts │ │ └── HighlightOverlay.css │ ├── DomainMessages │ │ └── typings.d.ts │ ├── EditorContext.tsx │ └── Modal.tsx ├── typings │ ├── draftjs-md-converter.d.ts │ ├── text-encoding.d.ts │ ├── simple-element-resize-detector.d.ts │ ├── json-schema-traverse.d.ts │ ├── streamsaver.d.ts │ ├── vtex.native-types.d.ts │ ├── draft-js.d.ts │ ├── pages.d.ts │ └── vtex.render-runtime.d.ts ├── __mocks__ │ ├── vtex.native-types.ts │ └── vtex.render-runtime.ts ├── queries │ ├── Languages.graphql │ ├── ContentIOMessage.graphql │ ├── UploadFile.graphql │ ├── DeleteRoute.graphql │ ├── AvailableTemplates.graphql │ ├── TenantInfo.graphql │ ├── RedirectWithoutBinding.graphql │ ├── DeleteRedirect.graphql │ ├── Redirect.graphql │ ├── MessagesForDomain.graphql │ ├── Redirects.graphql │ ├── SaveRedirect.graphql │ ├── ExtensionConfigurations.graphql │ ├── SaveRoute.graphql │ ├── Routes.graphql │ └── Route.graphql ├── utils │ ├── bindings │ │ ├── typings.d.ts │ │ └── index.ts │ ├── conditions │ │ ├── typings.d.ts │ │ └── index.ts │ ├── blocks │ │ └── typings.d.ts │ ├── auditEvents │ │ └── index.ts │ ├── AdminLoadingContext.tsx │ └── components │ │ └── typings.d.ts ├── .prettierrc ├── MediaGalleryWidget.tsx ├── HighlightOverlay.tsx ├── .eslintrc ├── EmptyExtensionPoint.tsx ├── EditableExtensionPoint.tsx ├── PageFormWrapper.tsx ├── Wrapper.tsx ├── StoreSettings.tsx ├── tsconfig.json ├── RedirectListWrapper.tsx └── PageEditor.tsx ├── pages └── plugins.json ├── crowdin.yml ├── .travis.yml ├── .vtexignore ├── .editorconfig ├── package.json ├── .vtex ├── deployment.yaml └── catalog-info.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── questions-and-help.md │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── docs └── CONTENT_PAGE.md └── manifest.json /react/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react-app"] 3 | } -------------------------------------------------------------------------------- /react/components/consts.ts: -------------------------------------------------------------------------------- 1 | export const ANIMATION_TIMEOUT = 250 2 | -------------------------------------------------------------------------------- /react/typings/draftjs-md-converter.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'draftjs-md-converter' 2 | -------------------------------------------------------------------------------- /pages/plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "storeWrapper > highlight-overlay": "highlight-overlay.cms" 3 | } -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/consts.ts: -------------------------------------------------------------------------------- 1 | export const NEW_CONFIGURATION_ID = 'new' 2 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /messages/en.json 3 | translation: /messages/%two_letters_code%.json 4 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/DeviceSwitcher.css: -------------------------------------------------------------------------------- 1 | .mr05 { 2 | margin-right: 1px; 3 | } 4 | -------------------------------------------------------------------------------- /react/__mocks__/vtex.native-types.ts: -------------------------------------------------------------------------------- 1 | export function formatIOMessage({ id }: { id: string }) { 2 | return id 3 | } 4 | -------------------------------------------------------------------------------- /react/components/admin/redirects/UploadModal/typings.d.ts: -------------------------------------------------------------------------------- 1 | export type ModalStates = 'UPLOAD_FILE' | 'LOADING' | 'ERROR' 2 | -------------------------------------------------------------------------------- /react/queries/Languages.graphql: -------------------------------------------------------------------------------- 1 | query Languages { 2 | languages { 3 | default 4 | supported 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /react/components/icons/DragHandle.css: -------------------------------------------------------------------------------- 1 | .editor-icon--fill path, 2 | .editor-icon--fill circle { 3 | fill: #727273; 4 | } 5 | -------------------------------------------------------------------------------- /react/queries/ContentIOMessage.graphql: -------------------------------------------------------------------------------- 1 | query ContentIOMessage($args: TranslateArgs!) { 2 | translate(args: $args) 3 | } 4 | -------------------------------------------------------------------------------- /react/utils/bindings/typings.d.ts: -------------------------------------------------------------------------------- 1 | export interface DropdownChangeInput { 2 | target: { 3 | value: string 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/__mocks__/vtex.render-runtime.ts: -------------------------------------------------------------------------------- 1 | export const global = {} 2 | export const Window = {} 3 | export const canUseDOM = true 4 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockSelector/BlockList/BlockListItem/Item.css: -------------------------------------------------------------------------------- 1 | .track-1 { 2 | letter-spacing: 0.2px; 3 | } 4 | -------------------------------------------------------------------------------- /react/queries/UploadFile.graphql: -------------------------------------------------------------------------------- 1 | mutation UploadFile($file: Upload!) { 2 | uploadFile(file: $file) { 3 | fileUrl 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "jsxBracketSameLine": false 6 | } 7 | -------------------------------------------------------------------------------- /react/queries/DeleteRoute.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteRoute($uuid: String!) { 2 | deleteRoute(uuid: $uuid, dataSource: "vtex.rewriter") 3 | } 4 | -------------------------------------------------------------------------------- /react/MediaGalleryWidget.tsx: -------------------------------------------------------------------------------- 1 | import MediaGalleryWidget from './components/MediaGalleryWidget/index' 2 | 3 | export default MediaGalleryWidget 4 | -------------------------------------------------------------------------------- /react/HighlightOverlay.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './components/HighlightOverlay' 2 | 3 | export { State } from './components/HighlightOverlay/typings' 4 | -------------------------------------------------------------------------------- /react/components/admin/pages/List/utils.ts: -------------------------------------------------------------------------------- 1 | export const sortRoutes = (routes: Route[]) => 2 | routes.sort((a, b) => a.interfaceId.localeCompare(b.interfaceId)) 3 | -------------------------------------------------------------------------------- /react/queries/AvailableTemplates.graphql: -------------------------------------------------------------------------------- 1 | query AvailableTemplates($interfaceId: String) { 2 | availableTemplates(interfaceId: $interfaceId) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true, 6 | "node": true 7 | }, 8 | "extends": "vtex-react" 9 | } 10 | -------------------------------------------------------------------------------- /react/components/ActionMenu/typings.d.ts: -------------------------------------------------------------------------------- 1 | export interface ActionMenuOption { 2 | isDangerous?: boolean 3 | label: string 4 | onClick: (e: ActionMenuOption) => void 5 | } 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationEditor/ConditionControls/typings.d.ts: -------------------------------------------------------------------------------- 1 | export interface DateRange { 2 | from?: Date 3 | to?: Date 4 | } 5 | -------------------------------------------------------------------------------- /react/typings/text-encoding.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'text-encoding' { 2 | declare const TextEncoder = class TextEnconder { 3 | public encode(data: string): void 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/graphql/DeleteFontFamily.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteFontFamily($id: String) { 2 | deleteFontFamily(id: $id) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/graphql/GenerateStyleSheet.graphql: -------------------------------------------------------------------------------- 1 | query GenerateStyleSheet($config: ConfigInput) { 2 | generateStyleSheet(config: $config) 3 | } 4 | -------------------------------------------------------------------------------- /react/components/admin/FormFieldSeparator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FormFieldSeparator = () =>
4 | 5 | export default FormFieldSeparator 6 | -------------------------------------------------------------------------------- /react/components/admin/pages/List/SectionSeparator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SectionSeparator = () =>
4 | 5 | export default SectionSeparator 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/Card/typings.d.ts: -------------------------------------------------------------------------------- 1 | export interface GetGenericContextArgs { 2 | context: PageContext 3 | isSitewide: boolean 4 | } 5 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/Transitions/consts.ts: -------------------------------------------------------------------------------- 1 | import { ANIMATION_TIMEOUT } from '../../../consts' 2 | 3 | export const COMMON_PROPS = { 4 | timeout: ANIMATION_TIMEOUT, 5 | } 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/graphql/DeleteStyle.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteStyle($id: String) { 2 | deleteStyle(id: $id) { 3 | id 4 | app 5 | name 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /react/utils/conditions/typings.d.ts: -------------------------------------------------------------------------------- 1 | export interface DateInfo { 2 | date?: Date 3 | from?: Date 4 | to?: Date 5 | } 6 | 7 | export type DateVerbOptions = 'between' | 'from' | 'is' | 'to' 8 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/graphql/RenameStyle.graphql: -------------------------------------------------------------------------------- 1 | mutation RenameStyle($id: String, $name: String) { 2 | renameStyle(id: $id, name: $name) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/components/admin/pages/SeparatorWithLine.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SeparatorWithLine = () =>
4 | 5 | export default SeparatorWithLine 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/colors.d.ts: -------------------------------------------------------------------------------- 1 | interface ColorInfo { 2 | path: string 3 | color: string 4 | configField: string 5 | } 6 | 7 | type Colors = Record 8 | -------------------------------------------------------------------------------- /react/components/admin/pages/List/typings.d.ts: -------------------------------------------------------------------------------- 1 | export interface CategorizedRoutes { 2 | multipleProducts: Route[] 3 | noProducts: Route[] 4 | singleProduct: Route[] 5 | notFoundSection: Route[] 6 | } 7 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/graphql/SaveSelectedStyle.graphql: -------------------------------------------------------------------------------- 1 | mutation SaveSelectedStyle($id: String) { 2 | saveSelectedStyle(id: $id) { 3 | id 4 | app 5 | name 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /react/components/admin/store/PWAForm/mutations/UpdatePWASettings.graphql: -------------------------------------------------------------------------------- 1 | mutation updatePWASettings($settings: PWASettingsInput) { 2 | updatePWASettings(settings: $settings) { 3 | disablePrompt 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/queries/TenantInfo.graphql: -------------------------------------------------------------------------------- 1 | query TenantInfo { 2 | tenantInfo { 3 | bindings { 4 | canonicalBaseAddress 5 | id 6 | supportedLocales 7 | targetProduct 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /react/typings/simple-element-resize-detector.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'simple-element-resize-detector' { 2 | export default function( 3 | el: Element, 4 | handler: (el: Element) => void 5 | ): HTMLIFrameElement 6 | } 7 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/Transitions/index.tsx: -------------------------------------------------------------------------------- 1 | import Enter from './Enter' 2 | import Exit from './Exit' 3 | 4 | const Transitions = { 5 | Enter, 6 | Exit, 7 | } 8 | 9 | export default Transitions 10 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/graphql/UpdateStyle.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateStyle($id: String, $config: ConfigInput) { 2 | updateStyle(id: $id, config: $config) { 3 | path 4 | selected 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /react/components/form/ImageUploader/ImagePreview.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | background: rgba(0, 0, 0, 0.15); 3 | opacity: 0; 4 | transition: opacity ease-in-out 100ms; 5 | } 6 | 7 | .overlay:hover { 8 | opacity: 1; 9 | } 10 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/graphql/CreateStyle.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateStyle($name: String, $config: ConfigInput) { 2 | createStyle(name: $name, config: $config) { 3 | id 4 | app 5 | name 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /react/components/admin/redirects/mutations/SaveRedirectFromFile.graphql: -------------------------------------------------------------------------------- 1 | mutation SaveManyRedirects($redirects: [RedirectInput!]!) { 2 | redirect @context(provider: "vtex.rewriter") { 3 | saveMany(routes: $redirects) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/components/admin/store/StoreForm/queries/InstalledApp.graphql: -------------------------------------------------------------------------------- 1 | query installedApp($slug: String!) { 2 | installedApp(slug: $slug) { 3 | slug 4 | name 5 | vendor 6 | version 7 | settings 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /react/utils/blocks/typings.d.ts: -------------------------------------------------------------------------------- 1 | export type BlockRole = 'after' | 'around' | 'before' 2 | 3 | export type BlockRolesForTree = BlockRole | 'blocks' 4 | 5 | export interface RelativeBlocks { 6 | [role: string]: string[] | undefined 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | yarn: true 5 | directories: 6 | - "node_modules" 7 | 8 | node_js: 9 | - "12" 10 | 11 | install: 12 | - yarn --cwd react 13 | - yarn 14 | 15 | script: 16 | cd react && yarn ci 17 | -------------------------------------------------------------------------------- /.vtexignore: -------------------------------------------------------------------------------- 1 | .git 2 | src 3 | 4 | .gitignore 5 | ./CHANGELOG.md 6 | README.md 7 | 8 | **/.DS_Store 9 | **/node_modules 10 | **/yarn-error.log 11 | 12 | **/*.{spec,test}.{ts,tsx} 13 | **/tests/** 14 | **/__fixtures__/** 15 | **/__mocks__/** 16 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationEditor/ConditionControls/Separator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Separator: React.FunctionComponent = () =>
4 | 5 | export default Separator 6 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Title: React.FunctionComponent = ({ children }) => ( 4 |

{children}

5 | ) 6 | 7 | export default Title 8 | -------------------------------------------------------------------------------- /react/components/admin/store/StoreForm/mutations/SaveAppSettings.graphql: -------------------------------------------------------------------------------- 1 | mutation saveAppSettings($app: String, $version: String, $settings: String) { 2 | saveAppSettings(app: $app, version: $version, settings: $settings) { 3 | message 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockSelector/BlockList/BlockListItem/ExpandArrow/styles.css: -------------------------------------------------------------------------------- 1 | .transition { 2 | transition-property: transform, color; 3 | transition-duration: 200ms; 4 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 5 | } 6 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/graphql/ListFonts.graphql: -------------------------------------------------------------------------------- 1 | query ListFonts { 2 | listFonts { 3 | id 4 | fontFamily 5 | fonts { 6 | id 7 | filename 8 | fontWeight 9 | fontStyle 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/graphql/appSettings.graphql: -------------------------------------------------------------------------------- 1 | query AppSettings($appName: String, $version: String) { 2 | appSettings(app: $appName, version: $version) 3 | @context(provider: "vtex.apps-graphql") { 4 | message 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/graphql/TenantInfo.graphql: -------------------------------------------------------------------------------- 1 | query TenantInfoQuery { 2 | tenantInfo { 3 | bindings { 4 | canonicalBaseAddress 5 | id 6 | supportedLocales 7 | targetProduct 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /react/components/admin/redirects/mutations/DeleteManyRedirectsFromFile.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteManyRedirects($paths: [String!]!, $locators: [RouteLocator!]) { 2 | redirect @context(provider: "vtex.rewriter") { 3 | deleteMany(paths: $paths, locators: $locators) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /react/components/admin/store/PWAForm/mutations/UpdateManifestIcon.graphql: -------------------------------------------------------------------------------- 1 | mutation updateManifest($icon: Upload!, $iOS: Boolean) { 2 | updateManifestIcon(icon: $icon, iOS: $iOS) { 3 | icons { 4 | src 5 | type 6 | sizes 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /react/components/admin/store/PWAForm/mutations/UpdateManifest.graphql: -------------------------------------------------------------------------------- 1 | mutation updateManifest($manifest: ManifestInput) { 2 | updateManifest(manifest: $manifest) { 3 | start_url 4 | theme_color 5 | background_color 6 | display 7 | orientation 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /react/components/admin/store/StoreForm/queries/AvailableApp.graphql: -------------------------------------------------------------------------------- 1 | query availableApp($id: String!) { 2 | availableApp(id: $id) { 3 | id 4 | title 5 | installed 6 | linked 7 | installedVersion 8 | settingsSchema 9 | settingsUiSchema 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /react/queries/RedirectWithoutBinding.graphql: -------------------------------------------------------------------------------- 1 | query RedirectWithoutBinding($path: String!) { 2 | redirect @context(provider: "vtex.rewriter") { 3 | get(path: $path) { 4 | binding 5 | endDate 6 | from 7 | to 8 | type 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /react/components/admin/institutional/Form/Loader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Spinner } from 'vtex.styleguide' 3 | 4 | const Loader = () => ( 5 |
6 | 7 |
8 | ) 9 | 10 | export default Loader 11 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/getLoginToggleState.ts: -------------------------------------------------------------------------------- 1 | import { State } from '../index' 2 | 3 | export const getLoginToggleState = (prevState: State) => ({ 4 | ...prevState, 5 | data: { 6 | ...prevState.data, 7 | auth: !!prevState.data && !prevState.data.auth, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /react/queries/DeleteRedirect.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteRedirect($path: String!, $binding: String!) { 2 | redirect @context(provider: "vtex.rewriter") { 3 | delete(path: $path, locator: { from: $path, binding: $binding }) { 4 | endDate 5 | from 6 | to 7 | type 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /react/queries/Redirect.graphql: -------------------------------------------------------------------------------- 1 | query Redirect($path: String!, $binding: String!) { 2 | redirect @context(provider: "vtex.rewriter") { 3 | get(path: $path, locator: { from: $path, binding: $binding }) { 4 | binding 5 | endDate 6 | from 7 | to 8 | type 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /react/typings/json-schema-traverse.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'json-schema-traverse' { 2 | const traverse: ( 3 | schema: ComponentSchema, 4 | opts: ( 5 | schema: JSONSchema6 & { widget: Widget }, 6 | JSONPointer: string 7 | ) => void 8 | ) => void 9 | 10 | export default traverse 11 | } 12 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/graphql/SaveFontFamily.graphql: -------------------------------------------------------------------------------- 1 | mutation SaveFontFamily($font: SaveFontFamilyInput) { 2 | saveFontFamily(font: $font) { 3 | id 4 | fontFamily 5 | fonts { 6 | filename 7 | fontWeight 8 | fontStyle 9 | id 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/__fixtures__/setupDate.ts: -------------------------------------------------------------------------------- 1 | import MockDate from 'mockdate' 2 | 3 | const setupDate = () => { 4 | beforeEach(() => { 5 | MockDate.set('2019-02-01') 6 | }) 7 | 8 | afterEach(() => { 9 | MockDate.reset() 10 | }) 11 | } 12 | 13 | export default setupDate 14 | -------------------------------------------------------------------------------- /react/components/form/RichText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import I18nInput from './I18nInput' 4 | import { CustomWidgetProps } from './typings' 5 | 6 | const RichText: React.FunctionComponent = props => ( 7 | 8 | ) 9 | 10 | export default RichText 11 | -------------------------------------------------------------------------------- /react/queries/MessagesForDomain.graphql: -------------------------------------------------------------------------------- 1 | query GetMessagesForDomain( 2 | $components: [String!]! 3 | $domain: String! 4 | $renderMajor: Int! 5 | ) { 6 | messages( 7 | components: $components 8 | domain: $domain 9 | renderMajor: $renderMajor 10 | ) { 11 | key 12 | message 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.21.2-beta.2", 3 | "license": "UNLICENSED", 4 | "scripts": { 5 | "test": "cd react && npm run test" 6 | }, 7 | "devDependencies": { 8 | "husky": "^3.0.4" 9 | }, 10 | "husky": { 11 | "hooks": { 12 | "pre-commit": "cd react && npx lint-staged" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /react/EmptyExtensionPoint.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | children?: React.ReactNode 5 | } 6 | 7 | // Backwards compatibility, we can delete this file upon releasing the next render-runtime. 8 | const EmptyExtensionPoint = ({ children }: Props) => children || null 9 | 10 | export default EmptyExtensionPoint 11 | -------------------------------------------------------------------------------- /react/EditableExtensionPoint.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | children?: React.ReactNode 5 | } 6 | 7 | // Backwards compatibility, we can delete this file upon releasing the next render-runtime. 8 | const EditableExtensionPoint = ({ children }: Props) => children || null 9 | 10 | export default EditableExtensionPoint 11 | -------------------------------------------------------------------------------- /react/components/EditorContainer/EditableText/styles.css: -------------------------------------------------------------------------------- 1 | .input::placeholder { 2 | color: #979899; 3 | font-style: normal; 4 | font-weight: 400; 5 | } 6 | 7 | .editableTextInputWrapper .editableTextInputWrapper__input { 8 | opacity: 0; 9 | } 10 | 11 | .editableTextInputWrapper:hover .editableTextInputWrapper__input { 12 | opacity: 1; 13 | } 14 | -------------------------------------------------------------------------------- /react/components/EditorContainer/graphql/DeleteContent.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteContent( 2 | $template: String 3 | $treePath: String 4 | $pageContext: PageContextInput 5 | $contentId: String 6 | ) { 7 | deleteContent( 8 | template: $template 9 | treePath: $treePath 10 | pageContext: $pageContext 11 | contentId: $contentId 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /react/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | 4 | const Loader = () => ( 5 | 6 | {text => ( 7 | 8 | {text} 9 | … 10 | 11 | )} 12 | 13 | ) 14 | 15 | export default Loader 16 | -------------------------------------------------------------------------------- /react/components/form/I18nInput/utils.ts: -------------------------------------------------------------------------------- 1 | export const isValidData = ( 2 | data: unknown 3 | ): data is { 4 | target: HTMLInputElement | HTMLTextAreaElement 5 | value: string 6 | } => 7 | typeof data === 'object' && 8 | data !== null && 9 | Object.prototype.hasOwnProperty.call(data, 'target') && 10 | Object.prototype.hasOwnProperty.call(data, 'value') 11 | -------------------------------------------------------------------------------- /react/queries/Redirects.graphql: -------------------------------------------------------------------------------- 1 | query Redirects($limit: Int, $next: String) { 2 | redirect @context(provider: "vtex.rewriter") { 3 | listRedirects(limit: $limit, next: $next) { 4 | routes { 5 | binding 6 | endDate 7 | from 8 | to 9 | type 10 | binding 11 | } 12 | next 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/ArrayFieldTemplateItem/PreviewOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './ArrayFieldTemplateItem.css' 4 | 5 | const PreviewOverlay = () => ( 6 |
9 | ) 10 | 11 | export default React.memo(PreviewOverlay) 12 | -------------------------------------------------------------------------------- /react/components/admin/institutional/utils.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AvailableApp, 3 | InstalledApp, 4 | } from '../store/StoreForm/components/withStoreSettings' 5 | 6 | export const parseStoreAppId = ( 7 | store: InstalledApp & AvailableApp & { settings: string } 8 | ) => { 9 | const appMajor = store.version.split('.')[0] 10 | return `${store.slug}@${appMajor}.x` 11 | } 12 | -------------------------------------------------------------------------------- /react/components/admin/redirects/ImportErrorModal/ImportErrorModal.css: -------------------------------------------------------------------------------- 1 | .import-error-modal-scroll::-webkit-scrollbar, 2 | .import-error-modal-scroll::-webkit-scrollbar-track { 3 | width: 2px; 4 | background: #f2f4f5; 5 | } 6 | 7 | .import-error-modal-scroll::-webkit-scrollbar-thumb { 8 | width: 2px; 9 | background: rgba(0, 0, 0, 0.2); 10 | border-radius: 2px; 11 | } 12 | -------------------------------------------------------------------------------- /react/components/form/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { WidgetProps } from 'react-jsonschema-form' 2 | 3 | export interface CustomWidgetProps extends WidgetProps { 4 | formContext: { 5 | messages: RenderContext['messages'] 6 | } 7 | onChange: (value: unknown) => void 8 | rawErrors?: string[] 9 | schema: WidgetProps['schema'] & { 10 | disabled?: boolean 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /react/components/icons/ContentActiveIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ContentActiveIcon: React.FC = () => ( 4 | 11 | 12 | 13 | ) 14 | 15 | export default ContentActiveIcon 16 | -------------------------------------------------------------------------------- /react/components/icons/ContentInactiveIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ContentInactiveIcon: React.FC = () => ( 4 | 11 | 12 | 13 | ) 14 | 15 | export default ContentInactiveIcon 16 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/Card/utils.ts: -------------------------------------------------------------------------------- 1 | import { GetGenericContextArgs } from './typings' 2 | 3 | export const getGenericContext = ({ 4 | context, 5 | isSitewide, 6 | }: GetGenericContextArgs) => { 7 | if (isSitewide) { 8 | return 'sitewide' 9 | } 10 | 11 | if (context.id === '*') { 12 | return 'template' 13 | } 14 | 15 | return 'page' 16 | } 17 | -------------------------------------------------------------------------------- /react/typings/streamsaver.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'streamsaver' { 2 | interface Constructable { 3 | new (): T 4 | } 5 | 6 | interface StreamSaver { 7 | static WritableStream: Constructable 8 | createWriteStream: (name: string) => WritableStream 9 | } 10 | 11 | let WritableStream: WritableStream 12 | const streamsaver: StreamSaver 13 | 14 | export default streamsaver 15 | } 16 | -------------------------------------------------------------------------------- /react/queries/SaveRedirect.graphql: -------------------------------------------------------------------------------- 1 | mutation saveRedirect( 2 | $endDate: String 3 | $from: String! 4 | $to: String! 5 | $type: RedirectTypes! 6 | $binding: String 7 | ) { 8 | redirect @context(provider: "vtex.rewriter") { 9 | save(route: { endDate: $endDate, from: $from, to: $to, type: $type, binding: $binding}) { 10 | endDate 11 | from 12 | to 13 | type 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/graphql/CopyBindings.graphql: -------------------------------------------------------------------------------- 1 | mutation CopyBindingContent($from: String!, $to: String!, $template: String, $context: PageContextInput) { 2 | copyBindingContent(from: $from, to: $to, template: $template, context: $context) { 3 | added { 4 | contentId 5 | origin 6 | } 7 | removed { 8 | contentId 9 | origin 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /react/components/admin/redirects/List/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { RedirectsQuery } from '../typings' 2 | 3 | export interface FetchMoreOptions { 4 | updateQuery: (prevData: RedirectsQuery, newData: UpdateQueryNewData) => void 5 | variables: QueryVariables 6 | } 7 | 8 | interface QueryVariables { 9 | from: number 10 | to: number 11 | } 12 | 13 | interface UpdateQueryNewData { 14 | fetchMoreResult?: RedirectsQuery 15 | } 16 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/SectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | 4 | interface Props { 5 | textId: string 6 | } 7 | 8 | const SectionTitle: React.FunctionComponent = ({ textId }) => ( 9 | 10 | {text =>

{text}

} 11 |
12 | ) 13 | 14 | export default SectionTitle 15 | -------------------------------------------------------------------------------- /react/components/RichTextEditor/Link.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Link as LinkProps } from 'draft-js' 4 | 5 | const Link = (props: LinkProps) => { 6 | const { contentState, entityKey, children } = props 7 | const { url } = contentState.getEntity(entityKey).getData() 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | 16 | export default Link 17 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/graphql/SaveRoute.graphql: -------------------------------------------------------------------------------- 1 | mutation SaveRoute($route: NewRouteInput) { 2 | saveRoute(route: $route) { 3 | auth 4 | blockId 5 | binding 6 | context 7 | declarer 8 | domain 9 | interfaceId 10 | path 11 | routeId 12 | uuid 13 | metaTags { 14 | description 15 | keywords 16 | robots 17 | } 18 | title 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | export const useHover = () => { 4 | const [hover, setHover] = useState(false) 5 | 6 | const handleMouseEnter = useCallback(() => { 7 | setHover(true) 8 | }, []) 9 | 10 | const handleMouseLeave = useCallback(() => { 11 | setHover(false) 12 | }, []) 13 | 14 | return { handleMouseEnter, handleMouseLeave, hover } 15 | } 16 | -------------------------------------------------------------------------------- /react/components/form/ImageUploader/styles.css: -------------------------------------------------------------------------------- 1 | .emptyStateContainer { 2 | width: 100%; 3 | height: 8rem; 4 | } 5 | 6 | .emptyState { 7 | width: 100%; 8 | height: 100%; 9 | text-align: center; 10 | margin: auto; 11 | } 12 | 13 | .emptyStateContainer:hover > div { 14 | color: #134cd8; 15 | border-color: #134cd8; 16 | width: 12.93rem; 17 | height: 7rem; 18 | } 19 | 20 | .imageUploaderText { 21 | max-width: 11.3rem; 22 | } 23 | -------------------------------------------------------------------------------- /react/components/icons/DangerIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const DangerIcon = () => ( 4 | 11 | 12 | 13 | 14 | ) 15 | 16 | export default React.memo(DangerIcon) 17 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/Card/styles.css: -------------------------------------------------------------------------------- 1 | /* This is a temporary hack to remove the hover background from the 2 | ActionMenu */ 3 | #action-menu-parent > div > div > div > button:active, 4 | #action-menu-parent > div > div > div > button:hover { 5 | background-color: transparent !important; 6 | } 7 | 8 | .editor-configuration-card-tag-editing > div { 9 | background: rgba(0, 0, 0, 0.5); 10 | } 11 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockSelector/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { SidebarComponent } from '../typings' 2 | 3 | export interface NormalizedBlock extends SidebarComponent { 4 | components: NormalizedBlock[] 5 | isEditable: boolean 6 | isSortable: boolean 7 | } 8 | 9 | export interface BlocksByRole { 10 | after: NormalizedBlock[] 11 | around: NormalizedBlock[] 12 | before: NormalizedBlock[] 13 | blocks: NormalizedBlock[] 14 | } 15 | -------------------------------------------------------------------------------- /react/components/admin/redirects/UploadModal/UploadPrompt/UploadPrompt.css: -------------------------------------------------------------------------------- 1 | .validation-errors-container { 2 | max-height: 16rem; 3 | } 4 | 5 | .validation-errors-container::-webkit-scrollbar, 6 | .validation-errors-container::-webkit-scrollbar-track { 7 | width: 4px; 8 | background: #f2f4f5; 9 | } 10 | 11 | .validation-errors-container::-webkit-scrollbar-thumb { 12 | width: 4px; 13 | background: rgba(0, 0, 0, 0.2); 14 | border-radius: 2px; 15 | } 16 | -------------------------------------------------------------------------------- /.vtex/deployment.yaml: -------------------------------------------------------------------------------- 1 | - name: admin-pages 2 | referenceId: 60JU5OHJ 3 | pipelines: 4 | - name: techdocs-v1 5 | parameters: 6 | entityReference: default/component/admin-pages 7 | indexFile: README.md 8 | when: 9 | - event: push 10 | source: branch 11 | regex: master 12 | path: 13 | - "docs/**" 14 | - ".vtex/deployment.yaml" 15 | - ".vtex/deployment.json" 16 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationEditor/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema6 } from 'json-schema' 2 | 3 | export interface ComponentFormState { 4 | depth: number 5 | onClose: () => void 6 | onTitleChange?: () => void 7 | title: string 8 | } 9 | 10 | export interface GetSchemasArgs { 11 | contentSchema?: JSONSchema6 12 | editTreePath: string | null 13 | iframeRuntime: RenderContext 14 | isContent?: boolean 15 | } 16 | -------------------------------------------------------------------------------- /react/components/admin/pages/consts.ts: -------------------------------------------------------------------------------- 1 | export const NEW_ROUTE_ID = 'new' 2 | 3 | export const ROUTES_FORM = 'admin.app.cms.pages.page-details' 4 | 5 | export const ROUTES_LIST = 'admin.app.cms.pages.page-list' 6 | 7 | export const INSTITUTIONAL_ROUTES_LIST = 'admin.app.cms.content' 8 | 9 | export const INSTITUTIONAL_ROUTES_FORM = 'admin.app.cms.content-details' 10 | 11 | export const WRAPPER_PATH = 'pages' 12 | 13 | export const DATA_SOURCE = 'vtex.rewriter' 14 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/getRemoveConditionalTemplateState.ts: -------------------------------------------------------------------------------- 1 | import { State } from '../index' 2 | 3 | export const getRemoveConditionalTemplateState = (uniqueId: number) => ( 4 | prevState: State 5 | ) => { 6 | const newPages = prevState.data.pages.filter( 7 | page => page.uniqueId !== uniqueId 8 | ) 9 | 10 | return { 11 | ...prevState, 12 | data: { ...prevState.data, pages: newPages }, 13 | formErrors: {}, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/Transitions/Enter/styles.css: -------------------------------------------------------------------------------- 1 | .transition-editor-enter-left { 2 | transform: translateX(-18em); 3 | } 4 | 5 | .transition-editor-enter-left-active { 6 | transition: transform 250ms ease-in; 7 | transform: translateX(0); 8 | } 9 | 10 | .transition-editor-enter-right { 11 | transform: translateX(18em); 12 | } 13 | 14 | .transition-editor-enter-right-active { 15 | transition: transform 250ms ease-in; 16 | transform: translateX(0); 17 | } 18 | -------------------------------------------------------------------------------- /react/components/EditorContainer/graphql/SendEventToAudit.graphql: -------------------------------------------------------------------------------- 1 | mutation SendEventToAudit($input: SendToAuditInput!) { 2 | sendEventToAudit(input: $input) { 3 | id 4 | mainAccountName 5 | date 6 | application 7 | accountName 8 | subjectId 9 | workspace 10 | operation 11 | meta { 12 | entityName 13 | entityBeforeAction 14 | entityAfterAction 15 | remoteIpAddress 16 | forwardFromVtexUserAgent 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { getIsDefaultContent } from './utils' 2 | 3 | describe('getIsDefaultContent', () => { 4 | it('should return true when origin is declared (comes from app)', () => { 5 | expect(getIsDefaultContent({ origin: 'comes from block' })).toBe(true) 6 | }) 7 | 8 | it('should return false when origin is null (declared by user)', () => { 9 | expect(getIsDefaultContent({ origin: null })).toBe(false) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/messages.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | desktop: { 3 | defaultMessage: 'Desktop view', 4 | id: 'admin/pages.editor.topbar.button.device.desktop.tooltip', 5 | }, 6 | mobile: { 7 | defaultMessage: 'Mobile view', 8 | id: 'admin/pages.editor.topbar.button.device.mobile.tooltip', 9 | }, 10 | tablet: { 11 | defaultMessage: 'Tablet view', 12 | id: 'admin/pages.editor.topbar.button.device.tablet.tooltip', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /react/components/admin/redirects/List/CreateButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { Button } from 'vtex.styleguide' 4 | 5 | interface Props { 6 | onClick: () => void 7 | } 8 | 9 | const CreateButton = ({ onClick }: Props) => ( 10 | 13 | ) 14 | 15 | export default CreateButton 16 | -------------------------------------------------------------------------------- /react/components/admin/AdminStructure.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageHeader } from 'vtex.styleguide' 3 | 4 | interface Props { 5 | title: string 6 | } 7 | 8 | const AdminStructure: React.FC = ({ children, title }) => ( 9 |
10 |
11 | 12 | 13 | {children} 14 |
15 |
16 | ) 17 | 18 | export default AdminStructure 19 | -------------------------------------------------------------------------------- /react/components/admin/redirects/UploadModal/UploadModal.css: -------------------------------------------------------------------------------- 1 | .full-modal-width { 2 | width: calc(100% + 6rem); 3 | } 4 | 5 | .error-container { 6 | max-height: 30vh; 7 | margin-bottom: -3rem; 8 | } 9 | 10 | .error-container::-webkit-scrollbar, 11 | .error-container::-webkit-scrollbar-track { 12 | width: 2px; 13 | background: #f2f4f5; 14 | } 15 | 16 | .error-container::-webkit-scrollbar-thumb { 17 | width: 2px; 18 | background: rgba(0, 0, 0, 0.2); 19 | border-radius: 2px; 20 | } 21 | -------------------------------------------------------------------------------- /react/components/admin/redirects/mutations/DeleteManyRedirectsFromFile.d.ts: -------------------------------------------------------------------------------- 1 | export interface DeleteManyRedirectsFromFileVariables { 2 | paths: string[] 3 | } 4 | 5 | export type DeleteManyRedirectsFromFileResult = boolean 6 | 7 | export type DeleteManyRedirectsFromFileFn = MutationFn< 8 | DeleteManyRedirectsFromFileVariables, 9 | DeleteManyRedirectsFromFileResult 10 | > 11 | 12 | export interface DeleteManyRedirectsProps { 13 | deleteManyRedirects: DeleteManyRedirectsFromFileFn 14 | } 15 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationEditor/ConditionControls/ConditionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | 4 | interface Props { 5 | labelId: string 6 | } 7 | 8 | const ConditionTitle: React.FunctionComponent = ({ labelId }) => ( 9 | 10 | {message =>
{message}
} 11 |
12 | ) 13 | 14 | export default React.memo(ConditionTitle) 15 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/Transitions/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { Transition } from 'react-transition-group' 2 | 3 | type Direction = 'left' | 'right' 4 | 5 | type TransitionProps = React.ComponentPropsWithoutRef 6 | 7 | interface Props { 8 | children: React.ReactElement 9 | condition: TransitionProps['in'] 10 | } 11 | 12 | export interface EnterProps extends Props { 13 | from: Direction 14 | } 15 | 16 | export interface ExitProps extends Props { 17 | to: Direction 18 | } 19 | -------------------------------------------------------------------------------- /react/components/admin/redirects/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { AlertProps } from 'vtex.styleguide' 2 | 3 | export interface RedirectsQuery { 4 | redirect: { 5 | list: Redirect[] 6 | numberOfEntries: number 7 | } 8 | } 9 | 10 | export interface AlertState { 11 | type: AlertProps['type'] 12 | message: string 13 | meta?: { 14 | failedRedirects: Redirect[] 15 | mutation: (data: Redirect[]) => Promise 16 | isSave: boolean 17 | } 18 | action?: { 19 | label: string 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /react/typings/vtex.native-types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vtex.native-types' { 2 | import { FunctionComponent } from 'react' 3 | import { FormattedMessage, InjectedIntlProps } from 'react-intl' 4 | 5 | const formatIOMessage: ( 6 | adaptedMessageDescriptor: FormattedMessage.MessageDescriptor & 7 | InjectedIntlProps, 8 | values?: Record 9 | ) => string 10 | 11 | export const IOMessage: FunctionComponent< 12 | FormattedMessage.Props & InjectedIntlProps 13 | > 14 | } 15 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/queries/ListStyles.ts: -------------------------------------------------------------------------------- 1 | import { Query, QueryResult } from 'react-apollo' 2 | import ListStyles from '../graphql/ListStyles.graphql' 3 | 4 | export interface ListStylesData { 5 | listStyles: Style[] 6 | } 7 | 8 | export type ListStylesQueryResult = QueryResult 9 | 10 | class ListStylesQuery extends Query { 11 | public static defaultProps = { 12 | query: ListStyles, 13 | } 14 | } 15 | 16 | export default ListStylesQuery 17 | -------------------------------------------------------------------------------- /react/queries/ExtensionConfigurations.graphql: -------------------------------------------------------------------------------- 1 | query ExtensionConfigurations( 2 | $configurationsIds: [String] 3 | $routeId: String! 4 | $treePath: String! 5 | $url: String! 6 | ) { 7 | extensionConfigurations( 8 | configurationsIds: $configurationsIds 9 | routeId: $routeId 10 | treePath: $treePath 11 | url: $url 12 | ) { 13 | allMatches 14 | conditions 15 | configurationId 16 | device 17 | label 18 | propsJSON 19 | routeId 20 | scope 21 | url 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /react/components/icons/ArrowIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | color?: string 5 | } 6 | 7 | const ArrowIcon: React.FunctionComponent = ({ color }) => ( 8 | 15 | 16 | 17 | ) 18 | 19 | ArrowIcon.defaultProps = { 20 | color: 'currentColor', 21 | } 22 | 23 | export default ArrowIcon 24 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/__fixtures__/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | data: { 3 | auth: false, 4 | blockId: 'test.app@1.x:store.home#cool-home', 5 | context: null, 6 | declarer: 'user', 7 | domain: 'store', 8 | interfaceId: 'test.app@1.x:store.home', 9 | pages: [], 10 | path: '/', 11 | routeId: 'store.home#cool-home', 12 | title: null, 13 | uuid: undefined, 14 | }, 15 | formErrors: {}, 16 | isDeletable: false, 17 | isInfoEditable: true, 18 | isLoading: false, 19 | } 20 | -------------------------------------------------------------------------------- /react/PageFormWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import PageForm, { Props } from './PageForm' 4 | import { useBinding } from './utils/bindings' 5 | 6 | const PageFormWrapper: React.FunctionComponent = (props: Props) => { 7 | const [localStorageBinding, setLocalStorageBinding] = useBinding() 8 | return ( 9 | 14 | ) 15 | } 16 | 17 | export default PageFormWrapper 18 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/typings/typings.d.ts: -------------------------------------------------------------------------------- 1 | interface BasicStyle { 2 | id: string 3 | app: string 4 | name: string 5 | } 6 | 7 | interface Style extends BasicStyle { 8 | editable: boolean 9 | selected: boolean 10 | path: string 11 | config: TachyonsConfig 12 | } 13 | 14 | interface ActionMenuOption { 15 | label: string 16 | onClick: (style: Style) => void 17 | } 18 | 19 | interface StyleAssetInfo { 20 | type: 'path' | 'stylesheet' 21 | keepSheet?: boolean 22 | selected?: boolean 23 | value: string 24 | } 25 | -------------------------------------------------------------------------------- /react/components/HighlightOverlay/OverlayMask/OverlayMask.css: -------------------------------------------------------------------------------- 1 | .overlay-mask-enter { 2 | opacity: 0; 3 | } 4 | 5 | .overlay-mask-enter-active { 6 | opacity: 0.8; 7 | transition: opacity 300ms cubic-bezier(0.19, 1, 0.22, 1); 8 | } 9 | 10 | .overlay-mask-enter-done { 11 | opacity: 0.8; 12 | } 13 | 14 | .overlay-mask-exit { 15 | opacity: 0.8; 16 | } 17 | 18 | .overlay-mask-exit-active { 19 | opacity: 0; 20 | transition: opacity 150ms cubic-bezier(0.215, 0.61, 0.355, 1); 21 | } 22 | 23 | .overlay-mask-exit-done { 24 | opacity: 0; 25 | } 26 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/ArrayFieldTemplateItem/Handle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SortableHandle } from 'react-sortable-hoc' 3 | 4 | import DragHandle from '../../../icons/DragHandle' 5 | import styles from '../styles.css' 6 | 7 | const Handle = SortableHandle(() => ( 8 |
11 | 12 |
13 | )) 14 | 15 | export default Handle 16 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/components/Colors.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | colors: string[] 5 | } 6 | 7 | const Colors: React.FunctionComponent = ({ colors }) => ( 8 |
9 | {colors.map((color, index) => ( 10 |
17 | ))} 18 |
19 | ) 20 | 21 | export default Colors 22 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/__fixtures__/newPage.ts: -------------------------------------------------------------------------------- 1 | import { ConditionsOperator } from 'vtex.styleguide' 2 | 3 | const newPage = { 4 | condition: { 5 | allMatches: true, 6 | id: '', 7 | statements: [ 8 | { 9 | error: '', 10 | object: { 11 | date: new Date('2019-02-01'), 12 | }, 13 | subject: 'date', 14 | verb: 'is', 15 | }, 16 | ], 17 | }, 18 | operator: 'all' as ConditionsOperator, 19 | pageId: '', 20 | template: '', 21 | } 22 | 23 | export default newPage 24 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/getLoginToggleState.test.ts: -------------------------------------------------------------------------------- 1 | import BASE_STATE from './__fixtures__/state' 2 | import { getLoginToggleState } from './getLoginToggleState' 3 | 4 | describe('getLoginToggleState', () => { 5 | it('should negate data.login value', () => { 6 | const mockState = { 7 | ...BASE_STATE, 8 | data: { 9 | ...BASE_STATE.data, 10 | auth: true, 11 | }, 12 | } 13 | 14 | expect(getLoginToggleState(mockState)).toMatchObject({ 15 | data: { auth: false }, 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /react/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { ToastProvider } from 'vtex.styleguide' 3 | import { AdminLoadingContextProvider } from './utils/AdminLoadingContext' 4 | 5 | interface Props { 6 | children?: ReactNode 7 | } 8 | 9 | const Wrapper: React.FunctionComponent = ({ children }) => ( 10 | 11 | 12 |
{children}
13 |
14 |
15 | ) 16 | 17 | export default React.memo(Wrapper) 18 | -------------------------------------------------------------------------------- /react/components/HighlightOverlay/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export interface State { 4 | editExtensionPoint: (treePath: string | null) => void 5 | editMode: boolean 6 | elementHeight?: HTMLElement['clientHeight'] 7 | highlightStyle?: CSSProperties 8 | labelStyle?: CSSProperties 9 | maskStyle?: CSSProperties 10 | openBlockTreePath: string | null 11 | highlightHandler: (treePath: string | null) => void 12 | highlightTreePath: string | null 13 | sidebarBlocksMap: Record 14 | } 15 | -------------------------------------------------------------------------------- /react/components/form/ImageUploader/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Alert } from 'vtex.styleguide' 3 | 4 | interface Props { 5 | message: string 6 | } 7 | 8 | const ErrorAlert: React.FunctionComponent = ({ message }) => { 9 | const [isVisible, setIsVisible] = useState(true) 10 | 11 | return isVisible ? ( 12 |
13 | setIsVisible(false)} type="error"> 14 | {message} 15 | 16 |
17 | ) : null 18 | } 19 | 20 | export default ErrorAlert 21 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { generateNewRouteId } from './utils' 2 | 3 | describe('#generateNewRouteId', () => { 4 | it('should remove trailing slash from path', () => { 5 | expect( 6 | generateNewRouteId('vtex.store@2.x:store.custom', '/path1/path2/') 7 | ).toBe('vtex.store@2.x:store.custom#path1-path2') 8 | }) 9 | 10 | it('should generate a routeId without "/"', () => { 11 | expect( 12 | generateNewRouteId('vtex.store@2.x:store.custom', '/path1/path2') 13 | ).toEqual(expect.not.stringContaining('/')) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /react/StoreSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { injectIntl, InjectedIntlProps } from 'react-intl' 3 | 4 | import AdminStructure from './components/admin/AdminStructure' 5 | import Store from './components/admin/store' 6 | 7 | const StoreSettings: React.FC = ({ intl }) => ( 8 | 14 | 15 | 16 | ) 17 | 18 | export default injectIntl(StoreSettings) 19 | -------------------------------------------------------------------------------- /react/components/EditorContainer/EditorContainer.css: -------------------------------------------------------------------------------- 1 | .h-3em { 2 | height: 48px; 3 | } 4 | 5 | .calc--height-relative { 6 | height: calc(100% - 3em); 7 | } 8 | 9 | .calc--height-relative--dev { 10 | height: calc(100% - 8.5em); 11 | } 12 | 13 | .mobile-preview { 14 | width: 360px; 15 | height: 640px; 16 | max-height: 100%; 17 | box-shadow: 1px 4px 8px rgba(0, 0, 0, 0.2); 18 | overflow: hidden; 19 | } 20 | 21 | .tablet-preview { 22 | width: 768px; 23 | height: 1024px; 24 | max-height: 100%; 25 | box-shadow: 1px 4px 8px rgba(0, 0, 0, 0.2); 26 | overflow: hidden; 27 | } 28 | -------------------------------------------------------------------------------- /react/components/icons/ComponentDragHandleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ComponentDragHandleIcon: React.FunctionComponent = () => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | 14 | export default ComponentDragHandleIcon 15 | -------------------------------------------------------------------------------- /react/components/RichTextEditor/Media.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Media as MediaProps } from 'draft-js' 4 | 5 | const Media = (props: MediaProps) => { 6 | const { contentState, block } = props 7 | const blockEntity = block.getEntityAt(0) 8 | 9 | if (!blockEntity) { 10 | return null 11 | } 12 | 13 | const entity = contentState.getEntity(blockEntity) 14 | const { src } = entity.getData() 15 | const type = entity.getType() 16 | 17 | if (type === 'IMAGE') { 18 | return {src} 19 | } 20 | 21 | return null 22 | } 23 | 24 | export default Media 25 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/queries/GenerateStyleSheet.ts: -------------------------------------------------------------------------------- 1 | import { Query } from 'react-apollo' 2 | import GenerateStyleSheet from '../graphql/GenerateStyleSheet.graphql' 3 | 4 | export interface GenerateStyleSheetData { 5 | generateStyleSheet: string 6 | } 7 | 8 | interface GenerateStyleSheetVariables { 9 | config: TachyonsConfig 10 | } 11 | 12 | class GenerateStyleSheetQuery extends Query< 13 | GenerateStyleSheetData, 14 | GenerateStyleSheetVariables 15 | > { 16 | public static defaultProps = { 17 | query: GenerateStyleSheet, 18 | } 19 | } 20 | 21 | export default GenerateStyleSheetQuery 22 | -------------------------------------------------------------------------------- /react/queries/SaveRoute.graphql: -------------------------------------------------------------------------------- 1 | mutation SaveRoute($route: NewRouteInput) { 2 | saveRoute(route: $route) { 3 | auth 4 | blockId 5 | binding 6 | context 7 | declarer 8 | domain 9 | interfaceId 10 | path 11 | routeId 12 | uuid 13 | metaTags { 14 | description 15 | keywords 16 | robots 17 | } 18 | pages { 19 | condition { 20 | allMatches 21 | id 22 | statements { 23 | subject 24 | verb 25 | objectJSON 26 | } 27 | } 28 | pageId 29 | template 30 | } 31 | title 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationEditor/styles.css: -------------------------------------------------------------------------------- 1 | .form { 2 | transition: transform 250ms ease-in-out; 3 | transform: translateX(0rem); 4 | } 5 | 6 | .form--leave { 7 | transition: transform 250ms ease-in-out; 8 | transform: translateX(-18rem); 9 | } 10 | 11 | .form--leave > :global(.rjsf) { 12 | height: 0; 13 | overflow: hidden; 14 | } 15 | 16 | @media only screen and (max-width: 40em) { 17 | .size-editor { 18 | bottom: 0; 19 | height: 100%; 20 | } 21 | } 22 | 23 | @media only screen and (min-width: 40em) { 24 | .size-editor { 25 | max-width: 450px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /react/components/icons/CalendarIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const CalendarIcon: React.FC = () => ( 4 | 5 | 9 | 10 | ) 11 | 12 | export default CalendarIcon 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/questions-and-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question you had while building a store 4 | labels: question 5 | --- 6 | 7 | **What are you trying to accomplish? Please describe.** 8 | A clear and concise description of what is your question and what you're trying to accomplish in the end is. 9 | 10 | **What have you tried so far?** 11 | A clear and concise description of what you tried to do already. 12 | 13 | **Additional info** 14 | Add extra content here (code samples, screenshots,...) 15 | 16 | | Account | Workspace | 17 | | -------------- | ---------------- | 18 | | `your account` | `your workspace` | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### SublimeText ### 2 | *.sublime-workspace 3 | 4 | ### OSX ### 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear on external disk 14 | .Spotlight-V100 15 | .Trashes 16 | 17 | ### Windows ### 18 | # Windows image file caches 19 | Thumbs.db 20 | ehthumbs.db 21 | 22 | # Folder config file 23 | Desktop.ini 24 | 25 | # Recycle Bin used on file shares 26 | $RECYCLE.BIN/ 27 | 28 | # App specific 29 | node_modules/ 30 | docs/_book/ 31 | .tmp 32 | .idea 33 | .vscode 34 | npm-debug.log 35 | .build/ 36 | lib 37 | *.orig 38 | package-lock.json 39 | yarn-error.log 40 | .eslintcache 41 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/typings.d.ts: -------------------------------------------------------------------------------- 1 | type DeepPartial = T extends object 2 | ? { [K in keyof T]?: DeepPartial } 3 | : T 4 | 5 | interface ButtonInfo { 6 | action?: () => void 7 | text: string 8 | } 9 | 10 | interface RouteInfo { 11 | backButton: ButtonInfo 12 | auxButton?: ButtonInfo 13 | title: string 14 | } 15 | 16 | interface NavigationUpdate { 17 | type: 'push' | 'pop' | 'update' 18 | route: EditorRoute 19 | } 20 | 21 | interface ColorRouteParams { 22 | id: string 23 | } 24 | 25 | interface CustomFontParams { 26 | id: string 27 | } 28 | 29 | interface TypeTokenParams { 30 | id: string 31 | } 32 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/getChangeTemplateConditionalTemplateState.ts: -------------------------------------------------------------------------------- 1 | import { State } from '../index' 2 | 3 | export const getChangeTemplateConditionalTemplateState = ( 4 | uniqueId: number, 5 | template: string 6 | ) => (prevState: State) => { 7 | const newPages = prevState.data.pages.map(page => { 8 | if (page.uniqueId === uniqueId) { 9 | return { 10 | ...page, 11 | template, 12 | } 13 | } 14 | return page 15 | }) 16 | 17 | return { 18 | ...prevState, 19 | data: { 20 | ...prevState.data, 21 | pages: newPages, 22 | }, 23 | formErrors: {}, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /react/queries/Routes.graphql: -------------------------------------------------------------------------------- 1 | query Routes($domain: String, $bindingId: String) { 2 | routes(domain: $domain, bindingId: $bindingId) { 3 | auth 4 | blockId 5 | binding 6 | context 7 | declarer 8 | domain 9 | interfaceId 10 | path 11 | routeId 12 | uuid 13 | metaTags { 14 | description 15 | keywords 16 | robots 17 | } 18 | pages { 19 | condition { 20 | allMatches 21 | id 22 | statements { 23 | subject 24 | verb 25 | objectJSON 26 | } 27 | } 28 | pageId 29 | template 30 | } 31 | title 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/Transitions/Exit/styles.css: -------------------------------------------------------------------------------- 1 | .transition-editor-exit-left { 2 | transform: translateX(0); 3 | } 4 | 5 | .transition-editor-exit-left-active { 6 | transition: transform 250ms ease-in; 7 | transform: translateX(-18em); 8 | } 9 | 10 | .transition-editor-exit-left-done { 11 | transform: translateX(-18em); 12 | } 13 | 14 | .transition-editor-exit-right { 15 | transform: translateX(0); 16 | } 17 | 18 | .transition-editor-exit-right-active { 19 | transition: transform 250ms ease-in; 20 | transform: translateX(18em); 21 | } 22 | 23 | .transition-editor-exit-right-done { 24 | transform: translateX(18em); 25 | } 26 | -------------------------------------------------------------------------------- /react/components/icons/utils.tsx: -------------------------------------------------------------------------------- 1 | export interface Dimensions { 2 | width: number 3 | height: number 4 | } 5 | 6 | export interface IconProps { 7 | color?: string 8 | size?: number 9 | block?: boolean 10 | } 11 | 12 | export const calcIconSize: (d: Dimensions, s: number) => Dimensions = ( 13 | iconBase, 14 | newSize 15 | ) => { 16 | const isHorizontal = iconBase.width >= iconBase.height 17 | 18 | const width = isHorizontal 19 | ? newSize 20 | : (newSize * iconBase.width) / iconBase.height 21 | 22 | const height = !isHorizontal 23 | ? newSize 24 | : (newSize * iconBase.height) / iconBase.width 25 | 26 | return { width, height } 27 | } 28 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/ArrayFieldTemplateItem/NoImagePlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | 4 | import IconImage from './icons/IconImage' 5 | 6 | const NoImagePlaceholder = () => ( 7 |
8 | 9 | 10 | 14 | {text =>

{text}

} 15 |
16 |
17 | ) 18 | 19 | export default React.memo(NoImagePlaceholder) 20 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/queries/ListFontsQuery.ts: -------------------------------------------------------------------------------- 1 | import { Query, QueryResult } from 'react-apollo' 2 | import ListFonts from '../graphql/ListFonts.graphql' 3 | import { FontFile } from '../mutations/SaveFontFamily' 4 | 5 | export interface FontFamily { 6 | id: string 7 | fontFamily: string 8 | fonts: FontFile[] 9 | } 10 | 11 | export interface ListFontsData { 12 | listFonts: FontFamily[] 13 | } 14 | 15 | export type ListFontsQueryResult = QueryResult 16 | 17 | class ListFontsQuery extends Query { 18 | public static defaultProps = { 19 | query: ListFonts, 20 | } 21 | } 22 | 23 | export default ListFontsQuery 24 | -------------------------------------------------------------------------------- /react/components/admin/store/PWAForm/queries/PWA.graphql: -------------------------------------------------------------------------------- 1 | query PWA { 2 | # Manifest 3 | manifest { 4 | start_url 5 | theme_color 6 | background_color 7 | display 8 | orientation 9 | icons { 10 | src 11 | type 12 | sizes 13 | } 14 | } 15 | iOSIcons { 16 | src 17 | type 18 | sizes 19 | } 20 | splashes { 21 | src 22 | type 23 | sizes 24 | } 25 | pwaSettings { 26 | disablePrompt 27 | promptOnCustomEvent 28 | } 29 | 30 | # Styles 31 | selectedStyle { 32 | config { 33 | semanticColors { 34 | background { 35 | base 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /react/queries/Route.graphql: -------------------------------------------------------------------------------- 1 | query Route($domain: String, $routeId: String!, $bindingId: String) { 2 | route(domain: $domain, routeId: $routeId, bindingId: $bindingId) { 3 | auth 4 | binding 5 | blockId 6 | context 7 | declarer 8 | domain 9 | interfaceId 10 | path 11 | routeId 12 | uuid 13 | metaTags { 14 | description 15 | keywords 16 | robots 17 | } 18 | pages { 19 | condition { 20 | allMatches 21 | id 22 | statements { 23 | subject 24 | verb 25 | objectJSON 26 | } 27 | } 28 | pageId 29 | template 30 | } 31 | title 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /react/components/icons/PersonIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PersonIcon: React.FC = () => ( 4 | 5 | 9 | 10 | 14 | 15 | ) 16 | 17 | export default PersonIcon 18 | -------------------------------------------------------------------------------- /react/components/icons/PageIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PageIcon: React.FC = () => ( 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | 25 | export default PageIcon 26 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/mutations/RenameStyle.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn } from 'react-apollo' 2 | import RenameStyle from '../graphql/RenameStyle.graphql' 3 | 4 | interface RenameStyleData { 5 | renameStyle: { 6 | id: string 7 | } 8 | } 9 | 10 | interface RenameStyleVariables { 11 | id: string 12 | name: string 13 | } 14 | 15 | export type RenameStyleFunction = MutationFn< 16 | RenameStyleData, 17 | RenameStyleVariables 18 | > 19 | 20 | class RenameStyleMutation extends Mutation< 21 | RenameStyleData, 22 | RenameStyleVariables 23 | > { 24 | public static defaultProps = { 25 | mutation: RenameStyle, 26 | } 27 | } 28 | 29 | export default RenameStyleMutation 30 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/components/Typography.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | typography: Font 5 | textColor?: string 6 | } 7 | 8 | const Typography: React.FunctionComponent = ({ 9 | typography: { 10 | fontFamily, 11 | fontSize, 12 | fontWeight, 13 | letterSpacing, 14 | textTransform, 15 | }, 16 | textColor, 17 | }) => { 18 | return ( 19 |
29 | Aa 30 |
31 | ) 32 | } 33 | 34 | export default Typography 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'New Feature' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. For example: I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getAddConditionalTemplateState, 3 | } from './getAddConditionalTemplateState' 4 | export { 5 | getChangeOperatorConditionalTemplateState, 6 | } from './getChangeOperatorConditionalTemplateState' 7 | export { 8 | getChangeStatementsConditionalTemplate, 9 | } from './getChangeStatementsConditionalTemplate' 10 | export { 11 | getChangeTemplateConditionalTemplateState, 12 | } from './getChangeTemplateConditionalTemplateState' 13 | export { getLoginToggleState } from './getLoginToggleState' 14 | export { 15 | getRemoveConditionalTemplateState, 16 | } from './getRemoveConditionalTemplateState' 17 | export { getValidateFormState } from './getValidateFormState' 18 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/mutations/DeleteStyle.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn } from 'react-apollo' 2 | import DeleteStyle from '../graphql/DeleteStyle.graphql' 3 | 4 | interface DeleteStyleData { 5 | deleteStyle: { 6 | id: string 7 | app: string 8 | name: string 9 | } 10 | } 11 | 12 | interface DeleteStyleVariables { 13 | id: string 14 | } 15 | 16 | export type DeleteStyleMutationFn = MutationFn< 17 | DeleteStyleData, 18 | DeleteStyleVariables 19 | > 20 | 21 | class DeleteStyleMutation extends Mutation< 22 | DeleteStyleData, 23 | DeleteStyleVariables 24 | > { 25 | public static defaultProps = { 26 | mutation: DeleteStyle, 27 | } 28 | } 29 | 30 | export default DeleteStyleMutation 31 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/mutations/UpdateStyle.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn } from 'react-apollo' 2 | import UpdateStyle from '../graphql/UpdateStyle.graphql' 3 | 4 | interface UpdateStyleData { 5 | updateStyle: { 6 | path: string 7 | selected: boolean 8 | } 9 | } 10 | 11 | interface UpdateStyleVariables { 12 | id: string 13 | config: TachyonsConfig 14 | } 15 | 16 | export type UpdateStyleFunction = MutationFn< 17 | UpdateStyleData, 18 | UpdateStyleVariables 19 | > 20 | 21 | class UpdateStyleMutation extends Mutation< 22 | UpdateStyleData, 23 | UpdateStyleVariables 24 | > { 25 | public static defaultProps = { 26 | mutation: UpdateStyle, 27 | } 28 | } 29 | 30 | export default UpdateStyleMutation 31 | -------------------------------------------------------------------------------- /react/utils/auditEvents/index.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | 3 | export const createEventObject = ( 4 | operation: string, 5 | entityName: string, 6 | account: string, 7 | workspace: string, 8 | subjectId?: any 9 | ) => { 10 | const event = { 11 | id: uuidv4(), 12 | date: new Date().toISOString(), 13 | mainAccountName: account, 14 | accountName: account, 15 | subjectId: subjectId ? subjectId : '', 16 | application: 'site-editor', 17 | workspace: workspace, 18 | operation, 19 | meta: { 20 | entityName, 21 | entityBeforeAction: '', 22 | entityAfterAction: '', 23 | remoteIpAddress: '0.0.0.0', 24 | forwardFromVtexUserAgent: 'cms', 25 | }, 26 | } 27 | 28 | return event 29 | } 30 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/Transitions/Exit/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CSSTransition } from 'react-transition-group' 3 | 4 | import { COMMON_PROPS } from '../consts' 5 | import { ExitProps } from '../typings' 6 | import styles from './styles.css' 7 | 8 | const Exit: React.FC = ({ children, condition, to }) => ( 9 | 19 | {children} 20 | 21 | ) 22 | 23 | export default React.memo(Exit) 24 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/mutations/CreateStyle.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn } from 'react-apollo' 2 | import CreateStyle from '../graphql/CreateStyle.graphql' 3 | 4 | interface CreateStyleData { 5 | createStyle: { 6 | id: string 7 | app: string 8 | name: string 9 | } 10 | } 11 | 12 | interface CreateStyleVariables { 13 | name: string 14 | config?: TachyonsConfig 15 | } 16 | 17 | export type CreateStyleMutationFn = MutationFn< 18 | CreateStyleData, 19 | CreateStyleVariables 20 | > 21 | 22 | class CreateStyleMutation extends Mutation< 23 | CreateStyleData, 24 | CreateStyleVariables 25 | > { 26 | public static defaultProps = { 27 | mutation: CreateStyle, 28 | } 29 | } 30 | 31 | export default CreateStyleMutation 32 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/getChangeStatementsConditionalTemplate.ts: -------------------------------------------------------------------------------- 1 | import { Statements } from 'pages' 2 | 3 | import { State } from '../index' 4 | 5 | export const getChangeStatementsConditionalTemplate = ( 6 | uniqueId: number, 7 | statements: Statements[] 8 | ) => (prevState: State) => { 9 | const newPages = prevState.data.pages.map(page => { 10 | if (page.uniqueId === uniqueId) { 11 | return { 12 | ...page, 13 | condition: { 14 | ...page.condition, 15 | statements, 16 | }, 17 | } 18 | } 19 | return page 20 | }) 21 | 22 | return { 23 | ...prevState, 24 | data: { 25 | ...prevState.data, 26 | pages: newPages, 27 | }, 28 | formErrors: {}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/icons/IconMobile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | color?: string 5 | } 6 | 7 | const IconPhone: React.FC = ({ color = 'currentColor' }) => ( 8 | 15 | 22 | 23 | 24 | ) 25 | 26 | export default IconPhone 27 | -------------------------------------------------------------------------------- /.vtex/catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: admin-pages 5 | description: The Pages Admin is a platform to dynamically edit a VTEX Store, 6 | making it possible to select editable components and change its 7 | configurations adding or removing content in a straightforward way. 8 | tags: 9 | - typescript 10 | - react 11 | annotations: 12 | vtex.com/janus-acronym: "" 13 | vtex.com/o11y-os-index: "" 14 | grafana/dashboard-selector: "" 15 | github.com/project-slug: vtex-apps/admin-pages 16 | vtex.com/platform-flow-id: "" 17 | backstage.io/techdocs-ref: dir:../ 18 | vtex.com/application-id: 60JU5OHJ 19 | spec: 20 | lifecycle: stable 21 | owner: te-0013 22 | type: frontend-ui 23 | dependsOn: [] 24 | -------------------------------------------------------------------------------- /react/components/HighlightOverlay/hooks/useStyles/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | import { ObserverReturnType } from '../useResizeObserver' 4 | import { State } from '../../typings' 5 | 6 | interface GetStylesParams { 7 | hasValidElement?: boolean 8 | highlightTreePath: State['highlightTreePath'] 9 | visibleElement?: Element 10 | } 11 | 12 | export type GetStyles = ( 13 | params: GetStylesParams 14 | ) => { 15 | highlightStyle: CSSProperties 16 | labelStyle: CSSProperties 17 | maskStyle: CSSProperties 18 | } 19 | 20 | interface UseStylesParams extends GetStylesParams, ObserverReturnType { 21 | isOverlayMaskActive: boolean 22 | setState: React.Dispatch> 23 | } 24 | 25 | export type UseStyles = (params: UseStylesParams) => void 26 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/icons/IconTablet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | color?: string 5 | } 6 | 7 | const IconTablet: React.FC = ({ color = 'currentColor' }) => ( 8 | 15 | 22 | 23 | 24 | ) 25 | 26 | export default IconTablet 27 | -------------------------------------------------------------------------------- /react/components/EditorContainer/graphql/ListContent.graphql: -------------------------------------------------------------------------------- 1 | query ListContent( 2 | $bindingId: String 3 | $blockId: String 4 | $pageContext: PageContextInput 5 | $template: String 6 | $treePath: String 7 | ) { 8 | listContentWithSchema( 9 | bindingId: $bindingId 10 | blockId: $blockId 11 | pageContext: $pageContext 12 | template: $template 13 | treePath: $treePath 14 | ) { 15 | content { 16 | condition { 17 | allMatches 18 | id 19 | pageContext { 20 | id 21 | type 22 | } 23 | statements { 24 | objectJSON 25 | subject 26 | verb 27 | } 28 | } 29 | contentId 30 | contentJSON 31 | label 32 | origin 33 | } 34 | schemaJSON 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /react/utils/AdminLoadingContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useContext } from 'react' 2 | 3 | const adminLoadingDefaultValue = { 4 | startLoading: () => { 5 | window.top.postMessage({ action: { type: 'START_LOADING' } }, '*') 6 | }, 7 | stopLoading: () => { 8 | window.top.postMessage({ action: { type: 'STOP_LOADING' } }, '*') 9 | }, 10 | } 11 | 12 | const AdminLoadingContext = React.createContext(adminLoadingDefaultValue) 13 | 14 | const useAdminLoadingContext = () => useContext(AdminLoadingContext) 15 | 16 | const AdminLoadingContextProvider: FunctionComponent = ({ children }) => ( 17 | 18 | {children} 19 | 20 | ) 21 | 22 | export { AdminLoadingContextProvider, useAdminLoadingContext } 23 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/mutations/SaveSelectedStyle.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn } from 'react-apollo' 2 | import SaveSelectedStyle from '../graphql/SaveSelectedStyle.graphql' 3 | 4 | interface SaveSelectedStyleData { 5 | saveSelectedStyle: { 6 | id: string 7 | app: string 8 | name: string 9 | } 10 | } 11 | 12 | interface SaveSelectedStyleVariables { 13 | id: string 14 | } 15 | 16 | export type SaveSelectedStyleMutationFn = MutationFn< 17 | SaveSelectedStyleData, 18 | SaveSelectedStyleVariables 19 | > 20 | 21 | class SaveSelectedStyleMutation extends Mutation< 22 | SaveSelectedStyleData, 23 | SaveSelectedStyleVariables 24 | > { 25 | public static defaultProps = { 26 | mutation: SaveSelectedStyle, 27 | } 28 | } 29 | 30 | export default SaveSelectedStyleMutation 31 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/CreateButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { ButtonWithIcon } from 'vtex.styleguide' 4 | 5 | import AddIcon from '../../../../icons/AddIcon' 6 | 7 | interface Props { 8 | onClick: (event: Event) => void 9 | } 10 | 11 | const CreateButton = ({ onClick }: Props) => ( 12 |
13 | } 16 | onClick={onClick} 17 | variation="tertiary" 18 | > 19 | 23 | 24 |
25 | ) 26 | 27 | export default React.memo(CreateButton) 28 | -------------------------------------------------------------------------------- /react/utils/conditions/index.ts: -------------------------------------------------------------------------------- 1 | import { ConditionsStatement } from 'vtex.styleguide' 2 | 3 | import { DateInfo, DateVerbOptions } from './typings' 4 | 5 | const formatDateInfo = (dateInfo: DateInfo, verb: DateVerbOptions) => 6 | ({ 7 | between: { 8 | from: dateInfo.from, 9 | to: dateInfo.to, 10 | }, 11 | from: { 12 | from: dateInfo.date, 13 | }, 14 | is: { 15 | from: dateInfo.date, 16 | }, 17 | to: { 18 | to: dateInfo.date, 19 | }, 20 | }[verb]) 21 | 22 | export const formatStatements = (statements: ConditionsStatement[]) => 23 | statements.map(({ object, subject, verb }) => ({ 24 | objectJSON: JSON.stringify( 25 | formatDateInfo(object as DateInfo, verb as DateVerbOptions) 26 | ), 27 | subject: subject as ConditionSubject, 28 | verb, 29 | })) 30 | -------------------------------------------------------------------------------- /react/components/EditorContainer/graphql/SaveContent.graphql: -------------------------------------------------------------------------------- 1 | mutation saveContent( 2 | $bindingId: String 3 | $blockId: String 4 | $configuration: ContentConfigurationInput 5 | $template: String 6 | $treePath: String 7 | $lang: String 8 | ) { 9 | saveContent( 10 | bindingId: $bindingId 11 | blockId: $blockId 12 | configuration: $configuration 13 | template: $template 14 | treePath: $treePath 15 | lang: $lang 16 | ) { 17 | # Available fields: 18 | # condition { 19 | # allMatches 20 | # id 21 | # pageContext { 22 | # id 23 | # type 24 | # } 25 | # statements { 26 | # objectJSON 27 | # subject 28 | # verb 29 | # } 30 | # } 31 | # contentJSON 32 | # contentId 33 | # label 34 | contentId 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/AddButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { ButtonWithIcon } from 'vtex.styleguide' 4 | 5 | import AddIcon from '../../icons/AddIcon' 6 | 7 | interface Props { 8 | onClick: (event: Event) => void 9 | } 10 | 11 | const icon = 12 | 13 | const AddButton: React.FC = ({ onClick }) => ( 14 |
15 | 16 | 20 | {text =>
{text}
} 21 |
22 |
23 |
24 | ) 25 | 26 | export default AddButton 27 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/ArrayFieldTemplateItem/ArrayFieldTemplateItem.css: -------------------------------------------------------------------------------- 1 | .action-menu-container { 2 | opacity: 0; 3 | transition: opacity ease-in 100ms; 4 | } 5 | 6 | .preview-container:hover .action-menu-container { 7 | opacity: 1; 8 | } 9 | 10 | .preview-container { 11 | height: 100%; 12 | } 13 | 14 | .multi-item { 15 | width: calc(100% - 3rem); 16 | } 17 | 18 | .single-item { 19 | width: calc(100% - 1rem); 20 | } 21 | 22 | .preview-text-container { 23 | width: 100%; 24 | } 25 | 26 | .preview-image { 27 | object-fit: cover; 28 | transition: filter ease-in 100ms; 29 | } 30 | 31 | .preview-overlay { 32 | background-color: black; 33 | opacity: 0; 34 | transition: opacity ease-in 100ms; 35 | will-change: opacity; 36 | } 37 | 38 | .preview-container:hover .preview-overlay { 39 | opacity: 0.15; 40 | } 41 | -------------------------------------------------------------------------------- /react/components/form/FieldTemplate.tsx: -------------------------------------------------------------------------------- 1 | import { JSONSchema6 } from 'json-schema' 2 | import React from 'react' 3 | import { FieldTemplateProps } from 'react-jsonschema-form' 4 | 5 | interface Props { 6 | children?: React.ReactNode 7 | classNames: string 8 | hidden?: boolean 9 | schema: JSONSchema6 10 | } 11 | 12 | const FieldTemplate: React.FunctionComponent = ({ 13 | children, 14 | classNames, 15 | hidden, 16 | schema, 17 | }) => { 18 | const isHidden = 19 | hidden || (schema.type !== 'object' && (schema as ComponentSchema).isLayout) 20 | 21 | if (isHidden) { 22 | return null 23 | } 24 | 25 | return
{children}
26 | } 27 | 28 | FieldTemplate.defaultProps = { 29 | classNames: '', 30 | hidden: false, 31 | } 32 | 33 | export default FieldTemplate 34 | -------------------------------------------------------------------------------- /react/components/icons/TemplateIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const TemplateIcon: React.FC = () => ( 4 | //
is used for making sure that the icon isn't vertically centered 5 |
6 | 7 | 14 | 15 | 22 | 23 | 24 | 25 |
26 | ) 27 | 28 | export default TemplateIcon 29 | -------------------------------------------------------------------------------- /react/components/DomainMessages/typings.d.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-client' 2 | import { InjectedIntl } from 'react-intl' 3 | 4 | export interface Props { 5 | runtime: RenderContext 6 | domain: string 7 | client: ApolloClient 8 | } 9 | 10 | export interface Message { 11 | key: string 12 | message: string 13 | } 14 | 15 | export interface Data { 16 | messages: Message[] 17 | } 18 | 19 | export interface Variables { 20 | components: string[] 21 | domain: string 22 | renderMajor: number 23 | } 24 | 25 | export interface AvailableCulturesProps { 26 | client: ApolloClient 27 | intl: InjectedIntl 28 | } 29 | 30 | export interface Languages { 31 | languages: { 32 | default: string 33 | supported: string[] 34 | } 35 | } 36 | 37 | export interface LabelledLocale { 38 | label: string 39 | value: string 40 | } 41 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/getChangeOperatorConditionalTemplateState.ts: -------------------------------------------------------------------------------- 1 | import { ConditionsProps } from 'vtex.styleguide' 2 | 3 | import { State } from '../index' 4 | 5 | export const getChangeOperatorConditionalTemplateState = ( 6 | uniqueId: number, 7 | operator: NonNullable 8 | ) => (prevState: State) => { 9 | const newPages = prevState.data.pages.map(page => { 10 | if (page.uniqueId === uniqueId) { 11 | return { 12 | ...page, 13 | condition: { 14 | ...page.condition, 15 | allMatches: operator === 'all', 16 | }, 17 | operator, 18 | } 19 | } 20 | return page 21 | }) 22 | 23 | return { 24 | ...prevState, 25 | data: { 26 | ...prevState.data, 27 | pages: newPages, 28 | }, 29 | formErrors: {}, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/Transitions/Enter/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CSSTransition } from 'react-transition-group' 3 | 4 | import { COMMON_PROPS } from '../consts' 5 | import { EnterProps } from '../typings' 6 | import styles from './styles.css' 7 | 8 | const Enter: React.FC = ({ children, condition, from }) => ( 9 | 21 | {children} 22 | 23 | ) 24 | 25 | export default React.memo(Enter) 26 | -------------------------------------------------------------------------------- /react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "esModuleInterop": true, 5 | "jsx": "preserve", 6 | "lib": [ 7 | "es2019", 8 | "dom", 9 | "es2018.promise", 10 | "esnext.asynciterable" 11 | ], 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "strictFunctionTypes": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "target": "es2017" 25 | }, 26 | "include": [ 27 | "./typings/*.d.ts", 28 | "./**/*.tsx", 29 | "./**/*.ts" 30 | ], 31 | "typeAcquisition": { 32 | "enable": false 33 | } 34 | } -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/graphql/GetRoute.graphql: -------------------------------------------------------------------------------- 1 | query GetRoute($routeId: String!) { 2 | route(routeId: $routeId, domain: "store") { 3 | auth 4 | blockId 5 | binding 6 | context 7 | declarer 8 | domain 9 | interfaceId 10 | conflicts { 11 | binding 12 | blockId 13 | interfaceId 14 | routeId 15 | } 16 | pages { 17 | pageId 18 | condition { 19 | id 20 | pageContext { 21 | id 22 | type 23 | } 24 | allMatches 25 | statements { 26 | subject 27 | verb 28 | objectJSON 29 | } 30 | } 31 | template 32 | } 33 | path 34 | routeId 35 | title 36 | uuid 37 | metaTags { 38 | description 39 | keywords 40 | robots 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /react/components/form/ObjectFieldTemplate.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { ObjectFieldTemplateProps } from 'react-jsonschema-form' 3 | 4 | const hasFieldToBeDisplayed = (field: ComponentSchema): boolean => { 5 | if (field.type !== 'object') { 6 | return !field.isLayout 7 | } 8 | 9 | return Object.values(field.properties || {}).reduce( 10 | (acc, currValue) => acc || hasFieldToBeDisplayed(currValue), 11 | false 12 | ) 13 | } 14 | 15 | const ObjectFieldTemplate: React.FunctionComponent< 16 | ObjectFieldTemplateProps 17 | > = ({ properties, schema }) => 18 | hasFieldToBeDisplayed(schema as ComponentSchema) ? ( 19 | {properties.map(property => property.content)} 20 | ) : null 21 | 22 | ObjectFieldTemplate.defaultProps = { 23 | properties: [], 24 | } 25 | 26 | export default ObjectFieldTemplate 27 | -------------------------------------------------------------------------------- /react/typings/draft-js.d.ts: -------------------------------------------------------------------------------- 1 | import 'draft-js' 2 | declare module 'draft-js' { 3 | export interface Media { 4 | block: ContentBlock 5 | blockProps: { [key: string]: unknown } | undefined 6 | blockStyleFn?: (block: ContentBlock) => string 7 | contentState: ContentState 8 | customStyleFn?: ( 9 | style: DraftInlineStyle, 10 | block: ContentBlock 11 | ) => DraftStyleMap 12 | customStyleMap: DraftStyleMap 13 | decorator: CompositeDecorator 14 | direction: string 15 | forceSelection: boolean 16 | offsetKey: string 17 | selection: SelectionState 18 | tree: Immutable.List 19 | } 20 | 21 | export interface Link { 22 | children: React.ElementType[] 23 | contentState: ContentState 24 | decoratedText: string 25 | dir: string | null 26 | entityKey: string 27 | offsetKey: string 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /react/components/ActionMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | ActionMenu as StyleguideActionMenu, 4 | IconOptionsDots, 5 | } from 'vtex.styleguide' 6 | 7 | import { ActionMenuOption } from './typings' 8 | 9 | interface Props { 10 | buttonSize?: string 11 | menuWidth?: number | string 12 | options: ActionMenuOption[] 13 | variation?: string 14 | } 15 | 16 | const icon = 17 | 18 | const ActionMenu: React.FunctionComponent = ({ 19 | buttonSize, 20 | menuWidth, 21 | options, 22 | variation = 'tertiary', 23 | }) => ( 24 | 35 | ) 36 | 37 | export default ActionMenu 38 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/consts.ts: -------------------------------------------------------------------------------- 1 | import IconDesktop from './icons/IconDesktop' 2 | import IconMobile from './icons/IconMobile' 3 | import IconTablet from './icons/IconTablet' 4 | 5 | const AVAILABLE_VIEWPORTS: Viewport[] = ['mobile', 'tablet', 'desktop'] 6 | 7 | const AVAILABLE_MOBILE_VIEWPORTS: Viewport[] = ['mobile', 'tablet'] 8 | 9 | export const BORDER_BY_POSITION = { 10 | first: 'br2 br--left b--transparent', 11 | last: 'br2 br--right b--transparent', 12 | middle: 'b--transparent', 13 | } 14 | 15 | export const ICON_BY_VIEWPORT = { 16 | desktop: IconDesktop, 17 | mobile: IconMobile, 18 | tablet: IconTablet, 19 | } 20 | 21 | export const VIEWPORTS_BY_DEVICE: Record< 22 | RenderContext['device'], 23 | Viewport[] 24 | > = { 25 | any: AVAILABLE_VIEWPORTS, 26 | desktop: AVAILABLE_VIEWPORTS, 27 | mobile: AVAILABLE_MOBILE_VIEWPORTS, 28 | } 29 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/icons/IconDesktop.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | color?: string 5 | } 6 | 7 | const IconDesktop: React.FC = ({ color = 'currentColor' }) => ( 8 | 15 | 22 | 29 | 30 | ) 31 | 32 | export default IconDesktop 33 | -------------------------------------------------------------------------------- /react/components/icons/ContentScheduledIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ContentScheduledIcon: React.FC = () => ( 4 | 11 | 19 | 20 | 28 | 29 | 30 | 31 | ) 32 | 33 | export default ContentScheduledIcon 34 | -------------------------------------------------------------------------------- /react/components/HighlightOverlay/HighlightOverlay.css: -------------------------------------------------------------------------------- 1 | .highlight-enter { 2 | opacity: 0; 3 | border-color: rgba(19, 76, 216, 0); 4 | } 5 | 6 | .highlight-enter-active { 7 | transition: opacity 150ms cubic-bezier(0.19, 1, 0.22, 1), 8 | border-color 150ms cubic-bezier(0.19, 1, 0.22, 1); 9 | opacity: 1; 10 | border-color: rgba(19, 76, 216, 1); 11 | } 12 | 13 | .highlight-enter-done { 14 | opacity: 1; 15 | border-color: rgba(19, 76, 216, 1); 16 | } 17 | 18 | .highlight-exit { 19 | opacity: 1; 20 | border-color: rgba(19, 76, 216, 1); 21 | } 22 | 23 | .highlight-exit-active { 24 | transition: opacity 150ms cubic-bezier(0.215, 0.61, 0.355, 1), 25 | border-color 150ms cubic-bezier(0.215, 0.61, 0.355, 1); 26 | opacity: 0; 27 | border-color: rgba(19, 76, 216, 0); 28 | } 29 | 30 | .highlight-exit-done { 31 | opacity: 0; 32 | border-color: rgba(19, 76, 216, 0); 33 | } 34 | -------------------------------------------------------------------------------- /react/RedirectListWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Query } from 'react-apollo' 3 | import { Tenant } from 'vtex.tenant-graphql' 4 | 5 | import Loader from './components/Loader' 6 | import TenantInfo from './queries/TenantInfo.graphql' 7 | import RedirectList from './RedirectList' 8 | import { getStoreBindings } from './utils/bindings' 9 | 10 | const RedirectListWrapper = () => { 11 | return ( 12 | query={TenantInfo}> 13 | {({ data, loading: isLoading }) => { 14 | const tenantInfo = data?.tenantInfo 15 | if (isLoading || !tenantInfo) { 16 | return 17 | } 18 | const storeBindings = getStoreBindings(tenantInfo) 19 | return 1} /> 20 | }} 21 | 22 | ) 23 | } 24 | 25 | export default React.memo(RedirectListWrapper) 26 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryResult } from 'react-apollo' 2 | 3 | export type DropdownOptions = { 4 | label: string 5 | value: string 6 | }[] 7 | 8 | export type DropdownValue = string 9 | 10 | type BaseAddress = string 11 | 12 | type Locale = string 13 | 14 | interface Binding { 15 | canonicalBaseAddress: string 16 | id: string 17 | supportedLocales: Locale[] 18 | targetProduct: string 19 | } 20 | 21 | interface TenantInfo { 22 | bindings: Binding[] 23 | } 24 | 25 | export interface TenantInfoData { 26 | tenantInfo: TenantInfo 27 | } 28 | 29 | interface QueryState extends Pick, 'data'> { 30 | hasError: boolean 31 | isLoading: QueryResult['loading'] 32 | } 33 | 34 | export type QueryStateReducer = ( 35 | prevState: QueryState, 36 | state: Partial 37 | ) => QueryState 38 | -------------------------------------------------------------------------------- /docs/CONTENT_PAGE.md: -------------------------------------------------------------------------------- 1 | # VTEX Custom Content 2 | 3 | ## Description 4 | 5 | This is just an easier way to create content pages in your store. It provides an editor for you to create new routes and their content in a single form. You can edit the content using the Side Editor later as well. 6 | 7 | :loudspeaker: **Disclaimer:** Don't fork this project; use, contribute, and/or open issues with your feature requests. 8 | 9 | ## Usage 10 | 11 | You must add `store.content` to your `blocks.json` just like the example below: 12 | 13 | ```json 14 | "store.content": { 15 | "children": ["flex-layout.row#content-body"] 16 | }, 17 | "flex-layout.row#content-body": { 18 | "children": ["rich-text"] 19 | } 20 | ``` 21 | 22 | ## Continuous Integrations 23 | 24 | ### Travis CI 25 | 26 | ![Build Status](https://travis-ci.org/vtex-apps/pages-editor.svg?branch=master)](https://travis-ci.org/vtex-apps/pages-editor) 27 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/Card/ConditionTags/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag as StyleguideTag } from 'vtex.styleguide' 3 | 4 | import CalendarIcon from '../../../../../../icons/CalendarIcon' 5 | import PersonIcon from '../../../../../../icons/PersonIcon' 6 | 7 | interface Props { 8 | kind: ConditionSubject 9 | text: string 10 | title: string 11 | } 12 | 13 | const iconByKind = { 14 | date: , 15 | utm: , 16 | } 17 | 18 | const Tag: React.FunctionComponent = ({ kind, text, title }) => ( 19 | 20 |
21 | {iconByKind[kind]} 22 | 23 | {title} 24 | 25 | {text} 26 |
27 |
28 | ) 29 | 30 | export default React.memo(Tag) 31 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/AbsoluteLoader.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames' 2 | import React from 'react' 3 | import { Spinner } from 'vtex.styleguide' 4 | 5 | import { useEditorContext } from '../../EditorContext' 6 | 7 | interface Props { 8 | containerClassName?: string 9 | } 10 | 11 | const AbsoluteLoader: React.FunctionComponent = ({ 12 | children, 13 | containerClassName, 14 | }) => { 15 | const editor = useEditorContext() 16 | 17 | const isLoading = editor.getIsLoading() 18 | 19 | return ( 20 | <> 21 | {isLoading ? ( 22 |
23 | 24 |
25 | ) : null} 26 | 27 |
28 | {children} 29 |
30 | 31 | ) 32 | } 33 | 34 | export default React.memo(AbsoluteLoader) 35 | -------------------------------------------------------------------------------- /react/components/admin/redirects/List/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Query } from 'react-apollo' 3 | import { Tenant } from 'vtex.tenant-graphql' 4 | 5 | import Loader from '../../../Loader' 6 | import List, { Props } from './List' 7 | import TenantInfo from '../../../../queries/TenantInfo.graphql' 8 | import { getStoreBindings } from '../../../../utils/bindings' 9 | 10 | const ListWrapper: React.FC> = props => { 11 | return ( 12 | query={TenantInfo}> 13 | {({ data, loading: isLoading }) => { 14 | const tenantInfo = data?.tenantInfo 15 | if (isLoading || !tenantInfo) { 16 | return 17 | } 18 | const storeBindings = getStoreBindings(tenantInfo) 19 | return 20 | }} 21 | 22 | ) 23 | } 24 | 25 | export default ListWrapper 26 | -------------------------------------------------------------------------------- /react/components/EditorContainer/mutations/DeleteContent.tsx: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn, MutationResult } from 'react-apollo' 2 | import DeleteContent from '../graphql/DeleteContent.graphql' 3 | 4 | export interface DeleteContentData { 5 | deleteContent: string 6 | } 7 | 8 | interface DeleteContentVariables { 9 | pageContext: PageContext 10 | contentId: string | null 11 | template: string 12 | treePath: string 13 | } 14 | 15 | export type DeleteContentMutationFn = MutationFn< 16 | DeleteContentData, 17 | DeleteContentVariables 18 | > 19 | 20 | export interface MutationRenderProps extends MutationResult { 21 | deleteContent: DeleteContentMutationFn 22 | } 23 | 24 | class DeleteContentMutation extends Mutation< 25 | DeleteContentData, 26 | DeleteContentVariables 27 | > { 28 | public static defaultProps = { 29 | mutation: DeleteContent, 30 | } 31 | } 32 | 33 | export default DeleteContentMutation 34 | -------------------------------------------------------------------------------- /react/components/EditorContainer/queries/ListContent.tsx: -------------------------------------------------------------------------------- 1 | import { Query, QueryResult } from 'react-apollo' 2 | import ListContent from '../graphql/ListContent.graphql' 3 | 4 | export const ListContentGraphqlDocument = ListContent 5 | 6 | export interface ListContentVariables { 7 | bindingId?: string 8 | blockId: string 9 | pageContext: { id: string; type: string } 10 | template: string 11 | treePath: string 12 | } 13 | 14 | export interface ListContentData { 15 | listContentWithSchema?: { 16 | content?: ExtensionConfiguration[] 17 | schemaJSON: string 18 | } 19 | } 20 | 21 | export type ListContentQueryResult = QueryResult< 22 | ListContentData, 23 | ListContentVariables 24 | > 25 | 26 | class ListContentQuery extends Query { 27 | public static defaultProps = { 28 | fetchPolicy: 'network-only', 29 | query: ListContent, 30 | } 31 | } 32 | 33 | export default ListContentQuery 34 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/utils/colors.ts: -------------------------------------------------------------------------------- 1 | import { forEachObjIndexed } from 'ramda' 2 | 3 | const fromSemanticColors = (semanticColors: SemanticColors): Colors => { 4 | const getFieldPath = (name: string) => { 5 | let fieldPath = name.split('_') 6 | if (fieldPath.length === 1) { 7 | fieldPath = ['default'].concat(fieldPath) 8 | } 9 | return fieldPath.reverse().join('.') 10 | } 11 | 12 | const colors: Colors = {} 13 | forEachObjIndexed((tokens: Tokens, field: string) => { 14 | forEachObjIndexed((color: string, token: string) => { 15 | if (token.startsWith('__')) { 16 | return 17 | } 18 | const info: ColorInfo = { 19 | color, 20 | configField: field, 21 | path: getFieldPath(field), 22 | } 23 | colors[token] = (colors[token] || []).concat(info) 24 | }, tokens) 25 | }, semanticColors) 26 | 27 | return colors 28 | } 29 | 30 | export default fromSemanticColors 31 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationEditor/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | 3 | import { ComponentFormState } from './typings' 4 | 5 | export function useComponentFormStateStack() { 6 | const [componentFormState, setComponentFormState] = useState< 7 | ComponentFormState | undefined 8 | >() 9 | const stack = useRef([]) 10 | 11 | function popComponentFormState() { 12 | stack.current.pop() 13 | 14 | setComponentFormState(stack.current[stack.current.length - 1]) 15 | } 16 | 17 | function pushComponentFormState(state: ComponentFormState) { 18 | const stateWithDepth = { ...state, depth: stack.current.length + 1 } 19 | 20 | stack.current.push(stateWithDepth) 21 | setComponentFormState(stateWithDepth) 22 | } 23 | 24 | return { 25 | currentDepth: stack.current.length, 26 | componentFormState, 27 | popComponentFormState, 28 | pushComponentFormState, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/hooks/useBinding.ts: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | 3 | import { Binding } from '../typings' 4 | 5 | const LAST_BINDING_KEY = 'vtex.site-editor.lastBinding' 6 | 7 | export const useBinding = (): [ 8 | Binding | undefined, 9 | React.Dispatch 10 | ] => { 11 | const [binding, setBinding] = React.useState(() => { 12 | const lastBinding = window.localStorage.getItem(LAST_BINDING_KEY) 13 | return lastBinding ? (JSON.parse(lastBinding) as Binding) : undefined 14 | }) 15 | 16 | return [ 17 | binding, 18 | useMemo(() => { 19 | return (value: Binding | undefined) => { 20 | if (value) { 21 | window.localStorage.setItem(LAST_BINDING_KEY, JSON.stringify(value)) 22 | } else { 23 | window.localStorage.removeItem(LAST_BINDING_KEY) 24 | } 25 | setBinding(value) 26 | } 27 | }, [setBinding]), 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /react/typings/pages.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pages' { 2 | import { ConditionsProps } from 'vtex.styleguide' 3 | 4 | type ConditionsOperators = NonNullable 5 | 6 | interface Statements { 7 | subject: string 8 | verb: string 9 | object: Record 10 | error: unknown 11 | } 12 | 13 | interface ConditionFormsData { 14 | id?: string 15 | allMatches: boolean 16 | statements: Statements[] 17 | } 18 | 19 | type PagesFormData = Omit & { 20 | condition: ConditionFormsData 21 | uniqueId: number 22 | operator: ConditionsOperators 23 | template: string 24 | } 25 | 26 | interface KeywordsFormData { 27 | value: string 28 | label: string 29 | } 30 | 31 | type RouteFormData = Omit & { 32 | pages: PagesFormData[] 33 | metaTagDescription?: string 34 | metaTagRobots?: string 35 | metaTagKeywords?: KeywordsFormData[] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /react/components/form/ImageUploader/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | 4 | import IconImage from '../../MediaGalleryWidget/icons/IconImage' 5 | import styles from './styles.css' 6 | 7 | const EmptyState = ({ style }: { style?: React.CSSProperties }) => { 8 | return ( 9 |
12 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 | ) 25 | } 26 | 27 | export default React.memo(EmptyState) 28 | -------------------------------------------------------------------------------- /react/components/admin/pages/utils.ts: -------------------------------------------------------------------------------- 1 | import startCase from 'lodash/startCase' 2 | 3 | type NewRouteTypeArg = Pick 4 | export const isNewRoute = (route: NewRouteTypeArg) => 5 | !route.uuid && !route.declarer 6 | 7 | export const isUserRoute = (route: NewRouteTypeArg) => !route.declarer 8 | 9 | type GetRouteTitleArg = Pick & 10 | NewRouteTypeArg 11 | 12 | export const getRouteTitle = (route: GetRouteTitleArg) => { 13 | const { blockId, declarer, interfaceId, title } = route 14 | 15 | const nameFromInterfaceOrBlock = startCase( 16 | blockId.split('#')[1] || 17 | interfaceId.split('.')[interfaceId.split('.').length - 1] 18 | ) 19 | 20 | if (isNewRoute(route)) { 21 | return title || '' 22 | } 23 | 24 | return declarer 25 | ? nameFromInterfaceOrBlock || title 26 | : title || nameFromInterfaceOrBlock 27 | } 28 | 29 | export const isStoreRoute = (domain: Route['domain']) => domain === 'store' 30 | -------------------------------------------------------------------------------- /react/components/form/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useIntl } from 'react-intl' 3 | import { formatIOMessage } from 'vtex.native-types' 4 | import { Toggle as StyleguideToggle } from 'vtex.styleguide' 5 | 6 | import { CustomWidgetProps } from './typings' 7 | 8 | const Toggle: React.FunctionComponent = ({ 9 | disabled, 10 | id, 11 | label, 12 | onChange, 13 | readonly, 14 | schema: { disabled: disabledBySchema }, 15 | value, 16 | }) => { 17 | const intl = useIntl() 18 | 19 | return ( 20 | ) => 26 | onChange(event.target.checked) 27 | } 28 | /> 29 | ) 30 | } 31 | 32 | Toggle.defaultProps = { 33 | disabled: false, 34 | readonly: false, 35 | } 36 | 37 | export default Toggle 38 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/icons/IconView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | color?: string 5 | } 6 | 7 | const IconView: React.FC = ({ color = 'currentColor' }) => ( 8 | 15 | 23 | 31 | 32 | ) 33 | 34 | export default IconView 35 | -------------------------------------------------------------------------------- /react/components/EditorContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | export const initialEditorContextState: EditorContextType = { 4 | activeConditions: [], 5 | addCondition: () => {}, 6 | allMatches: true, 7 | availableCultures: [], 8 | blockData: {}, 9 | editExtensionPoint: () => {}, 10 | editMode: false, 11 | editTreePath: null, 12 | getIsLoading: () => false, 13 | iframeWindow: window.self, 14 | isSidebarVisible: true, 15 | messages: {}, 16 | mode: 'content', 17 | onChangeIframeUrl: () => {}, 18 | setDevice: () => {}, 19 | setIsLoading: () => {}, 20 | setMode: () => {}, 21 | setBlockData: () => {}, 22 | setViewport: () => {}, 23 | toggleEditMode: () => {}, 24 | toggleSidebarVisibility: () => {}, 25 | viewport: 'desktop', 26 | } 27 | 28 | export const EditorContext = createContext(initialEditorContextState) 29 | 30 | EditorContext.displayName = 'EditorContext' 31 | 32 | export const useEditorContext = () => useContext(EditorContext) 33 | -------------------------------------------------------------------------------- /react/components/icons/AddIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const AddIcon: React.FunctionComponent = () => ( 4 | 5 | 13 | 21 | 29 | 30 | ) 31 | 32 | export default AddIcon 33 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/utils.test.ts: -------------------------------------------------------------------------------- 1 | import EXTENSIONS from './__fixtures__/extensions' 2 | import { getIsSitewide } from './utils' 3 | 4 | describe('getIsSitewide', () => { 5 | it('should return true for AFTER', () => { 6 | const mockEditTreePath = 'store.home/$after_footer' 7 | 8 | expect(getIsSitewide(EXTENSIONS, mockEditTreePath)).toBe(true) 9 | }) 10 | 11 | it('should return true for AROUND', () => { 12 | const mockEditTreePath = 'store.home/$around_homeWrapper' 13 | 14 | expect(getIsSitewide(EXTENSIONS, mockEditTreePath)).toBe(true) 15 | }) 16 | 17 | it('should return true for BEFORE', () => { 18 | const mockEditTreePath = 'store.home/$before_header.full' 19 | 20 | expect(getIsSitewide(EXTENSIONS, mockEditTreePath)).toBe(true) 21 | }) 22 | 23 | it('should return false for blocks without role', () => { 24 | const mockEditTreePath = 'store.home/carousel#home' 25 | 26 | expect(getIsSitewide(EXTENSIONS, mockEditTreePath)).toBe(false) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /react/components/admin/redirects/mutations/SaveRedirectFromFile.tsx: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn, MutationResult } from 'react-apollo' 2 | 3 | import SaveRedirectFromFile from './SaveRedirectFromFile.graphql' 4 | 5 | interface SaveRedirectFromFileData { 6 | saveRedirectFromFile: boolean 7 | } 8 | 9 | export type UploadActionType = 'save' | 'delete' 10 | 11 | interface SaveRedirectFromFileVariables { 12 | redirects: Redirect[] 13 | } 14 | 15 | type SaveRedirectFromFileMutationFn = MutationFn< 16 | SaveRedirectFromFileData, 17 | SaveRedirectFromFileVariables 18 | > 19 | 20 | export interface MutationRenderProps 21 | extends MutationResult { 22 | saveRedirectFromFile: SaveRedirectFromFileMutationFn 23 | } 24 | 25 | class SaveRedirectFromFileMutation extends Mutation< 26 | SaveRedirectFromFileData, 27 | SaveRedirectFromFileVariables 28 | > { 29 | public static defaultProps = { 30 | mutation: SaveRedirectFromFile, 31 | } 32 | } 33 | 34 | export default SaveRedirectFromFileMutation 35 | -------------------------------------------------------------------------------- /react/components/icons/EarthIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const EarthIcon: React.FC = () => ( 4 | 5 | 9 | 10 | 14 | 15 | ) 16 | 17 | export default EarthIcon 18 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/icons/IconPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | color?: string 5 | } 6 | 7 | const IconEdit: React.FC = ({ color = 'currentColor' }) => ( 8 | 15 | 22 | 28 | 29 | ) 30 | 31 | export default IconEdit 32 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/icons/CopyContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | color?: string 5 | } 6 | 7 | const CopyContent = ({ color = 'currentColor' }: Props) => ( 8 | 14 | 22 | 30 | 31 | ) 32 | 33 | export default CopyContent 34 | -------------------------------------------------------------------------------- /react/components/EditorContainer/mutations/SaveContent.tsx: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn, MutationResult } from 'react-apollo' 2 | import SaveContent from '../graphql/SaveContent.graphql' 3 | 4 | interface SaveContentData { 5 | saveContent: Pick 6 | } 7 | 8 | interface SaveContentVariables { 9 | bindingId?: string 10 | blockId?: string 11 | configuration: Omit & { 12 | contentId: string | null 13 | } 14 | lang: RenderContext['culture']['locale'] 15 | template: string 16 | treePath: string 17 | } 18 | 19 | export type SaveContentMutationFn = MutationFn< 20 | SaveContentData, 21 | SaveContentVariables 22 | > 23 | 24 | export interface MutationRenderProps extends MutationResult { 25 | SaveContent: SaveContentMutationFn 26 | } 27 | 28 | class SaveContentMutation extends Mutation< 29 | SaveContentData, 30 | SaveContentVariables 31 | > { 32 | public static defaultProps = { 33 | mutation: SaveContent, 34 | } 35 | } 36 | 37 | export default SaveContentMutation 38 | -------------------------------------------------------------------------------- /react/components/admin/institutional/Form/UnallowedWarning.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { Button, EmptyState } from 'vtex.styleguide' 4 | 5 | const UnallowedWarning = () => ( 6 | 12 | } 13 | > 14 | 15 | {message =>

{message}

} 16 |
17 |
18 | 29 |
30 |
31 | ) 32 | 33 | export default UnallowedWarning 34 | -------------------------------------------------------------------------------- /react/components/admin/redirects/consts.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = '/admin/app/cms/redirects' 2 | export const CSV_SEPARATOR = ';' 3 | 4 | export const getCSVHeader = (hasMultipleBindings: boolean) => 5 | hasMultipleBindings ? 'from;to;type;binding;endDate' : 'from;to;type;endDate' 6 | 7 | const CSV_SINGLE_BINDING_TEMPLATE = `/temporary;/;TEMPORARY; 8 | /temporary-with-date;/;TEMPORARY;2025-01-01T00:00:00.000Z; 9 | /permanent;/;PERMANENT; 10 | ` 11 | const CSV_MULTIPLE_BINDING_TEMPLATE = `/temporary;/;TEMPORARY; 12 | /temporary-with-date;/;TEMPORARY;1234_binding_id;2025-01-01T00:00:00.000Z; 13 | /permanent;/;PERMANENT;123_binding_id; 14 | ` 15 | 16 | export const getCSVTemplate = (hasMultipleBindings: boolean) => 17 | `${getCSVHeader(hasMultipleBindings)} 18 | ${ 19 | hasMultipleBindings 20 | ? CSV_MULTIPLE_BINDING_TEMPLATE 21 | : CSV_SINGLE_BINDING_TEMPLATE 22 | }` 23 | 24 | export const NEW_REDIRECT_ID = 'new' 25 | 26 | export const PAGINATION_START = 0 27 | export const PAGINATION_STEP = 10 28 | export const REDIRECTS_LIMIT = 500 29 | 30 | export const WRAPPER_PATH = 'redirects' 31 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/typography/TypeTokensEntry.tsx: -------------------------------------------------------------------------------- 1 | import { useKeydownFromClick } from 'keydown-from-click' 2 | import startCase from 'lodash/startCase' 3 | import React from 'react' 4 | import { RouteComponentProps } from 'react-router' 5 | 6 | import { EditorPath, IdParam } from '../StyleEditorRouter' 7 | 8 | interface EntryProps { 9 | name: string 10 | history: RouteComponentProps['history'] 11 | } 12 | 13 | const TypeTokenEntry: React.FunctionComponent = ({ 14 | name, 15 | history, 16 | }) => { 17 | const handleClick = React.useCallback( 18 | () => history.push(EditorPath.typeToken.replace(IdParam, name)), 19 | [history, name] 20 | ) 21 | 22 | const handleKeyDown = useKeydownFromClick(handleClick) 23 | 24 | return ( 25 |
32 | {startCase(name)} 33 |
34 | ) 35 | } 36 | 37 | export default TypeTokenEntry 38 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/typings/config.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FontFamilyProperty, 3 | FontSizeProperty, 4 | FontWeightProperty, 5 | LetterSpacingProperty, 6 | TextTransformProperty, 7 | } from 'csstype' 8 | 9 | declare global { 10 | interface Font { 11 | fontFamily: FontFamilyProperty 12 | fontWeight: FontWeightProperty 13 | fontSize: FontSizeProperty 14 | textTransform: TextTransformProperty 15 | letterSpacing: LetterSpacingProperty 16 | } 17 | 18 | interface Tokens { 19 | [token: string]: string 20 | } 21 | 22 | interface SemanticColors { 23 | [field: string]: Tokens 24 | } 25 | 26 | interface TypographyStyles { 27 | heading_1: Font 28 | heading_2: Font 29 | heading_3: Font 30 | heading_4: Font 31 | heading_5: Font 32 | heading_6: Font 33 | body: Font 34 | small: Font 35 | mini: Font 36 | action: Font 37 | action__small: Font 38 | action__large: Font 39 | code: Font 40 | } 41 | 42 | interface TachyonsConfig { 43 | semanticColors: SemanticColors 44 | typography: { 45 | styles: TypographyStyles 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /react/components/admin/TargetPathContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType, useContext } from 'react' 2 | 3 | export interface TargetPathContextProps { 4 | targetPath: string 5 | setTargetPath: (s: string) => void 6 | } 7 | 8 | const TargetPathContext = React.createContext({ 9 | setTargetPath: () => undefined, 10 | targetPath: '', 11 | }) 12 | const useTargetPathContext = () => useContext(TargetPathContext) 13 | 14 | const withTargetPath = ( 15 | Component: ComponentType 16 | ): ComponentType => { 17 | const extendedComponent = (props: TOriginalProps) => ( 18 | 19 | {({ setTargetPath, targetPath }) => { 20 | return ( 21 | 26 | ) 27 | }} 28 | 29 | ) 30 | return extendedComponent 31 | } 32 | 33 | export { useTargetPathContext, withTargetPath } 34 | export default TargetPathContext 35 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/AvailableEditor.tsx: -------------------------------------------------------------------------------- 1 | import { useKeydownFromClick } from 'keydown-from-click' 2 | import React from 'react' 3 | import { FormattedMessage } from 'react-intl' 4 | import { RouteComponentProps, withRouter } from 'react-router-dom' 5 | 6 | interface Props extends RouteComponentProps { 7 | path: string 8 | titleId: string 9 | widget?: React.ReactNode 10 | } 11 | 12 | const AvailableEditor: React.FunctionComponent = ({ 13 | history, 14 | path, 15 | titleId, 16 | widget, 17 | }) => { 18 | const redirect = React.useCallback(() => history.push(path), [history, path]) 19 | 20 | const redirectByKeyDown = useKeydownFromClick(redirect) 21 | 22 | return ( 23 |
30 | 31 | 32 | 33 | {widget} 34 |
35 | ) 36 | } 37 | 38 | export default withRouter(AvailableEditor) 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Desktop environment:** 25 | 26 | 33 | 34 | **Smartphone environment:** 35 | 36 | - Device: [e.g. iPhone6] 37 | - OS: [e.g. iOS8.1] 38 | - Browser [e.g. stock browser, safari] 39 | - Version [e.g. 22] 40 | 41 | **Additional context** 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /react/components/RichTextEditor/style.css: -------------------------------------------------------------------------------- 1 | .RichEditor_root { 2 | background: #fff; 3 | border: 1px solid #ddd; 4 | font-size: 14px; 5 | padding: 15px; 6 | } 7 | 8 | .RichEditor_editor { 9 | border-top: 1px solid #ddd; 10 | cursor: text; 11 | font-size: 16px; 12 | min-height: 20rem; 13 | padding: 10px; 14 | } 15 | 16 | .RichEditor_editor div[data-contents='true'] { 17 | min-height: 20em; 18 | } 19 | 20 | .RichEditor_editor .public-DraftEditorPlaceholder-root, 21 | .RichEditor_editor .public-DraftEditor-content { 22 | margin: 0 -15px -15px; 23 | padding: 15px; 24 | } 25 | 26 | .RichEditor_hidePlaceholder .public-DraftEditorPlaceholder-root { 27 | display: none; 28 | } 29 | 30 | .RichEditor_editor .RichEditor_blockquote { 31 | border-left: 5px solid #eee; 32 | color: #666; 33 | font-family: 'Hoefler Text', 'Georgia', serif; 34 | font-style: italic; 35 | margin: 16px 0; 36 | padding: 10px 20px; 37 | } 38 | 39 | .RichEditor_editor .public-DraftStyleDefault-pre { 40 | background-color: rgba(0, 0, 0, 0.05); 41 | font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace; 42 | font-size: 16px; 43 | padding: 20px; 44 | } 45 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/stateHandlers/getAddConditionalTemplateState.ts: -------------------------------------------------------------------------------- 1 | import { PagesFormData } from 'pages' 2 | 3 | import { State } from '../index' 4 | 5 | const getMaxUniqueId: (pages: PagesFormData[]) => number = pages => { 6 | return pages.reduce((acc, { uniqueId: currentValue }) => { 7 | if (acc < currentValue) { 8 | return currentValue 9 | } 10 | return acc 11 | }, -1) 12 | } 13 | 14 | export const getAddConditionalTemplateState = (prevState: State) => { 15 | const maxUniqueId = getMaxUniqueId(prevState.data.pages) 16 | const now = new Date() 17 | const newPage: PagesFormData = { 18 | condition: { 19 | allMatches: true, 20 | id: '', 21 | statements: [ 22 | { 23 | error: '', 24 | object: { date: now }, 25 | subject: 'date', 26 | verb: 'is', 27 | }, 28 | ], 29 | }, 30 | operator: 'all', 31 | pageId: '', 32 | template: '', 33 | uniqueId: maxUniqueId + 1, 34 | } 35 | 36 | return { 37 | ...prevState, 38 | data: { ...prevState.data, pages: prevState.data.pages.concat(newPage) }, 39 | formErrors: {}, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /react/components/admin/redirects/UploadModal/UploadPrompt/validateRedirect.ts: -------------------------------------------------------------------------------- 1 | import { defineMessages, IntlShape } from 'react-intl' 2 | 3 | export function isRedirectType(obj: unknown): obj is Redirect { 4 | return ( 5 | typeof obj === 'object' && 6 | obj !== null && 7 | 'endDate' in obj && 8 | 'from' in obj && 9 | 'to' in obj && 10 | 'type' in obj && 11 | typeof (obj as Record).type === 'string' 12 | ) 13 | } 14 | 15 | const messages = defineMessages({ 16 | invalidTypeError: { 17 | defaultMessage: 18 | '[Line {line}] Invalid value "{type}" for redirect type. Allowed values are "PERMANENT" and "TEMPORARY".', 19 | id: 'admin/pages.admin.redirects.upload-modal.prompt.validation-error.type', 20 | }, 21 | }) 22 | 23 | export function validateRedirect( 24 | redirect: Redirect, 25 | line: number, 26 | intl: IntlShape 27 | ): string | undefined { 28 | const { type } = redirect 29 | return type.toUpperCase() !== 'PERMANENT' && 30 | type.toUpperCase() !== 'TEMPORARY' 31 | ? intl.formatMessage(messages.invalidTypeError, { 32 | line, 33 | type, 34 | }) 35 | : undefined 36 | } 37 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockSelector/BlockList/BlockListItem/ExpandArrow/index.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames' 2 | import { useKeydownFromClick } from 'keydown-from-click' 3 | import React from 'react' 4 | 5 | import ArrowIcon from '../../../../../../icons/ArrowIcon' 6 | import styles from './styles.css' 7 | 8 | interface Props { 9 | isExpanded: boolean 10 | onClick: () => void 11 | } 12 | 13 | const ExpandArrow: React.FC = ({ isExpanded, onClick }) => { 14 | const handleKeyDown = useKeydownFromClick(onClick) 15 | 16 | return ( 17 |
24 |
25 |
30 | 31 |
32 |
33 |
34 | ) 35 | } 36 | 37 | export default ExpandArrow 38 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleList/icons/CreateNewIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // TODO: Use tachyons on stroke color 4 | const CreateNewIcon: React.FunctionComponent = () => ( 5 | 12 | 20 | 28 | 36 | 37 | ) 38 | 39 | export default CreateNewIcon 40 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/typography/TypeTokensList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { RouteComponentProps } from 'react-router' 4 | 5 | import StyleEditorHeader from '../StyleEditorHeader' 6 | import TypeTokenEntry from './TypeTokensEntry' 7 | 8 | interface Props extends RouteComponentProps { 9 | style: Style 10 | } 11 | 12 | const TypeTokensList: React.FunctionComponent = ({ history, style }) => { 13 | const title = ( 14 | 18 | ) 19 | 20 | const { styles } = style.config.typography 21 | const tokens = Object.entries(styles) 22 | .filter(([name]) => !name.startsWith('__')) 23 | .map(([name, value]) => ({ ...value, name })) 24 | 25 | return ( 26 | <> 27 | 28 |
29 | {tokens.map(({ name }) => ( 30 | 31 | ))} 32 |
33 | 34 | ) 35 | } 36 | 37 | export default TypeTokensList 38 | -------------------------------------------------------------------------------- /react/components/HighlightOverlay/OverlayMask/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CSSTransition } from 'react-transition-group' 3 | import styles from './OverlayMask.css' 4 | 5 | interface Props { 6 | isActive: boolean 7 | style?: React.CSSProperties 8 | } 9 | 10 | const classNames = { 11 | enter: styles['overlay-mask-enter'], 12 | enterActive: styles['overlay-mask-enter-active'], 13 | enterDone: styles['overlay-mask-enter-done'], 14 | exit: styles['overlay-mask-exit'], 15 | exitActive: styles['overlay-mask-exit-active'], 16 | exitDone: styles['overlay-mask-exit-done'], 17 | } 18 | 19 | const timeout = { 20 | enter: 300, 21 | exit: 150, 22 | } 23 | 24 | export default function OverlayMask({ style, isActive }: Props) { 25 | const overlayMaskStyle: React.CSSProperties = isActive 26 | ? {} 27 | : { pointerEvents: 'none' } 28 | 29 | return ( 30 | 36 |
40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /react/components/admin/institutional/Form/utils.tsx: -------------------------------------------------------------------------------- 1 | import { RouteFormData } from 'pages' 2 | import { State } from './index' 3 | 4 | import { slugify } from '../../utils' 5 | 6 | const requiredMessage = 'admin/pages.admin.pages.form.templates.field.required' 7 | 8 | const validateFalsyPath = (path: keyof RouteFormData) => ( 9 | data: RouteFormData 10 | ) => !data[path] && { [path]: requiredMessage } 11 | 12 | const validatePathUrl = (path: string) => { 13 | if (!path) return { path: requiredMessage } 14 | if (!path.startsWith('/')) 15 | return { 16 | path: 'admin/pages.admin.pages.form.templates.path.validation-error', 17 | } 18 | if (slugify(path) !== path.slice(1)) 19 | return { 20 | path: 'admin/pages.admin.pages.form.templates.path.invalid-format', 21 | } 22 | 23 | return {} 24 | } 25 | 26 | export const getValidateFormState = (prevState: State) => { 27 | return { 28 | ...prevState, 29 | formErrors: { 30 | ...prevState.formErrors, 31 | ...(validatePathUrl(prevState.data.path) as Record), 32 | ...(prevState.isInfoEditable 33 | ? validateFalsyPath('title')(prevState.data) 34 | : {}), 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /react/components/admin/pages/List/Entry.tsx: -------------------------------------------------------------------------------- 1 | import { useKeydownFromClick } from 'keydown-from-click' 2 | import React from 'react' 3 | import { withRuntimeContext } from 'vtex.render-runtime' 4 | import { IconEdit } from 'vtex.styleguide' 5 | 6 | import { ROUTES_FORM } from '../consts' 7 | import { getRouteTitle } from '../utils' 8 | 9 | interface Props { 10 | route: Route 11 | runtime: RenderContext 12 | } 13 | 14 | const Entry = ({ route, runtime }: Props) => { 15 | const handleClick = React.useCallback(() => { 16 | runtime.navigate({ 17 | page: ROUTES_FORM, 18 | params: { id: encodeURIComponent(route.routeId) }, 19 | }) 20 | }, [route.routeId, runtime]) 21 | 22 | const handleKeyDown = useKeydownFromClick(handleClick) 23 | 24 | return ( 25 |
26 |
{getRouteTitle(route)}
27 |
34 | 35 |
36 |
37 | ) 38 | } 39 | 40 | export default withRuntimeContext(Entry) 41 | -------------------------------------------------------------------------------- /react/components/RichTextEditor/StyleButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Tooltip } from 'vtex.styleguide' 4 | 5 | interface StyleBtnProps { 6 | label: string | JSX.Element 7 | title?: string | JSX.Element 8 | active: boolean 9 | onToggle: (style: string | null) => void 10 | style: string | null 11 | className?: string 12 | } 13 | 14 | const StyleButton = ({ 15 | active, 16 | onToggle, 17 | label, 18 | style, 19 | className, 20 | title, 21 | }: StyleBtnProps) => { 22 | const handleToggle = (e: React.MouseEvent) => { 23 | e.preventDefault() 24 | onToggle(style) 25 | } 26 | 27 | return ( 28 | 29 |
37 | {label} 38 |
39 |
40 | ) 41 | } 42 | 43 | StyleButton.defaultProps = { 44 | title: '', 45 | className: '', 46 | } 47 | 48 | export default StyleButton 49 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/ArrayFieldTemplateItem/icons/IconImage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | size?: number 5 | stroke?: string 6 | } 7 | 8 | const ImageIcon: React.FunctionComponent = ({ size, stroke }) => ( 9 | 16 | 25 | 31 | 32 | 36 | 37 | 38 | 39 | ) 40 | 41 | ImageIcon.defaultProps = { 42 | size: 16, 43 | stroke: '#979899', 44 | } 45 | 46 | export default React.memo(ImageIcon) 47 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/typography/TypographyEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { defineMessages, FormattedMessage } from 'react-intl' 3 | 4 | import AvailableEditor from '../AvailableEditor' 5 | import StyleEditorHeader from '../StyleEditorHeader' 6 | import { EditorPath } from '../StyleEditorRouter' 7 | 8 | defineMessages({ 9 | fontFamilyTitle: { 10 | defaultMessage: 'Font Family', 11 | id: 'admin/pages.editor.styles.edit.font-family.title', 12 | }, 13 | }) 14 | 15 | const TypographyEditor: React.FunctionComponent = () => { 16 | const title = ( 17 | 21 | ) 22 | 23 | return ( 24 | <> 25 | 26 |
27 | 31 | 35 |
36 | 37 | ) 38 | } 39 | 40 | export default TypographyEditor 41 | -------------------------------------------------------------------------------- /react/components/admin/institutional/List/Entry.tsx: -------------------------------------------------------------------------------- 1 | import { useKeydownFromClick } from 'keydown-from-click' 2 | import React from 'react' 3 | import { withRuntimeContext } from 'vtex.render-runtime' 4 | import { IconEdit } from 'vtex.styleguide' 5 | 6 | import { INSTITUTIONAL_ROUTES_FORM } from '../../pages/consts' 7 | import { getRouteTitle } from '../../pages/utils' 8 | 9 | interface Props { 10 | route: Route 11 | runtime: RenderContext 12 | } 13 | 14 | const Entry = ({ route, runtime }: Props) => { 15 | const handleClick = React.useCallback(() => { 16 | runtime.navigate({ 17 | page: INSTITUTIONAL_ROUTES_FORM, 18 | params: { id: encodeURIComponent(route.routeId) }, 19 | }) 20 | }, [route.routeId, runtime]) 21 | 22 | const handleKeyDown = useKeydownFromClick(handleClick) 23 | 24 | return ( 25 |
26 |
{getRouteTitle(route)}
27 |
34 | 35 |
36 |
37 | ) 38 | } 39 | 40 | export default withRuntimeContext(Entry) 41 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ToastConsumer } from 'vtex.styleguide' 3 | 4 | import BlockPicker from './BlockPicker' 5 | import ContextSelectors from './ContextSelectors' 6 | import DeviceSwitcher from './DeviceSwitcher' 7 | import SidebarVisibilityToggle from './SidebarVisibilityToggle' 8 | import UrlInput from './UrlInput' 9 | 10 | import styles from '../EditorContainer.css' 11 | 12 | interface Props { 13 | iframeRuntime: RenderContext 14 | } 15 | 16 | const Topbar: React.FunctionComponent = ({ iframeRuntime }) => ( 17 |
20 | 21 | {({ showToast }) => ( 22 | 23 | )} 24 | 25 | 26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 | ) 43 | 44 | export default Topbar 45 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/ItemTransitions.css: -------------------------------------------------------------------------------- 1 | .item-enter { 2 | transform: translateX(36rem); 3 | } 4 | 5 | .item-enter-active { 6 | transform: translateX(18rem); 7 | transition: transform 250ms ease-in-out; 8 | } 9 | 10 | .item-enter-done { 11 | transform: translateX(18rem); 12 | } 13 | 14 | .item-exit { 15 | transform: translateX(18rem); 16 | } 17 | 18 | .item-exit-active { 19 | transform: translateX(36rem); 20 | transition: transform 250ms ease-in-out; 21 | } 22 | 23 | .item-exit-done { 24 | transform: translateX(36rem); 25 | } 26 | 27 | .item-depth-enter { 28 | transform: translateX(18rem); 29 | } 30 | 31 | .item-depth-enter-active { 32 | transform: translateX(0rem); 33 | transition: transform 250ms ease-in-out; 34 | } 35 | 36 | .item-depth-enter-done { 37 | transform: translateX(0rem); 38 | } 39 | 40 | /* So the hidden item doesn't affect the height of the current form */ 41 | .item-depth-enter-done > :global(.vtex-admin-pages-4-x .form-group) { 42 | height: 0; 43 | overflow: hidden; 44 | } 45 | 46 | .item-depth-exit { 47 | transform: translateX(0rem); 48 | } 49 | 50 | .item-depth-exit-active { 51 | transform: translateX(18rem); 52 | transition: transform 250ms ease-in-out; 53 | } 54 | 55 | .item-depth-exit-done { 56 | transform: translateX(18rem); 57 | } 58 | -------------------------------------------------------------------------------- /react/components/icons/DragHandle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { calcIconSize } from './utils' 3 | 4 | import styles from './DragHandle.css' 5 | 6 | interface Props { 7 | className?: string 8 | size?: number 9 | } 10 | 11 | const iconBase = { 12 | height: 12, 13 | width: 6, 14 | } 15 | 16 | const DragHandle: React.FunctionComponent = ({ 17 | size, 18 | className, 19 | ...props 20 | }) => { 21 | const newSize = calcIconSize(iconBase, size as number) // TS doesn't detect defaultProps 22 | 23 | return ( 24 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | DragHandle.defaultProps = { 44 | size: 20, 45 | } 46 | 47 | export default React.memo(DragHandle) 48 | -------------------------------------------------------------------------------- /react/utils/components/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema6 } from 'json-schema' 2 | import { RenderComponent } from 'vtex.render-runtime' 3 | 4 | export type PropsOrContent = Extension['content'] | Extension['props'] 5 | 6 | export interface GetActiveContentIdParams { 7 | extensions: RenderContext['extensions'] 8 | treePath: EditorContextType['editTreePath'] 9 | } 10 | 11 | export interface GetComponentSchemaParams { 12 | component: RenderComponent | null 13 | contentSchema?: JSONSchema6 14 | propsOrContent: PropsOrContent 15 | runtime: RenderContext 16 | isContent: boolean 17 | } 18 | 19 | export interface GetSchemaPropsOrContentParams { 20 | messages?: RenderContext['messages'] 21 | schema?: JSONSchema6Definition 22 | propsOrContent?: Record 23 | } 24 | 25 | export interface GetSchemaPropsOrContentFromRuntimeParams { 26 | component: RenderComponent | null 27 | contentSchema?: JSONSchema6 28 | isContent?: boolean 29 | messages?: RenderContext['messages'] 30 | propsOrContent: PropsOrContent 31 | runtime: RenderContext 32 | } 33 | 34 | export interface TranslateMessageParams { 35 | dictionary: Record 36 | id?: string 37 | } 38 | 39 | export interface UpdateExtensionFromFormParams { 40 | data: object 41 | isContent?: boolean 42 | runtime: RenderContext 43 | treePath: string | null 44 | } 45 | -------------------------------------------------------------------------------- /react/utils/bindings/index.ts: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { Binding, Tenant } from 'vtex.tenant-graphql' 3 | 4 | const STORE_PRODUCT = 'vtex-storefront' 5 | const LAST_BINDING_KEY = 'vtex.cms-pages.lastBinding' 6 | 7 | export const getStoreBindings = (tenant: Tenant) => { 8 | return tenant.bindings.filter( 9 | binding => binding.targetProduct === STORE_PRODUCT 10 | ) 11 | } 12 | 13 | export const getBindingSelectorOptions = (bindings: Binding[]) => 14 | bindings.map(binding => ({ 15 | label: binding.canonicalBaseAddress, 16 | value: binding.id, 17 | })) 18 | 19 | export const useBinding = (): [ 20 | Binding | undefined, 21 | React.Dispatch 22 | ] => { 23 | const [binding, setBinding] = React.useState(() => { 24 | const lastBinding = window.localStorage.getItem(LAST_BINDING_KEY) 25 | return lastBinding ? (JSON.parse(lastBinding) as Binding) : undefined 26 | }) 27 | 28 | return [ 29 | binding, 30 | useMemo(() => { 31 | return (value: Binding | undefined) => { 32 | if (value) { 33 | window.localStorage.setItem(LAST_BINDING_KEY, JSON.stringify(value)) 34 | } else { 35 | window.localStorage.removeItem(LAST_BINDING_KEY) 36 | } 37 | setBinding(value) 38 | } 39 | }, [setBinding]), 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /react/components/EditorContainer/mutations/SendEventToAudit.tsx: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationFn, MutationResult } from 'react-apollo' 2 | 3 | import SendEventToAudit from '../graphql/SendEventToAudit.graphql' 4 | 5 | interface SendEventToAuditData { 6 | sendEventToAudit: any 7 | } 8 | 9 | interface MetaData { 10 | entityName: string 11 | entityBeforeAction: string 12 | entityAfterAction: string 13 | remoteIpAddress: string 14 | forwardFromVtexUserAgent: string 15 | } 16 | 17 | interface SendEventToAuditInput { 18 | id: string 19 | date: string 20 | mainAccountName: string 21 | accountName: string 22 | subjectId: string 23 | application: string 24 | workspace: string 25 | operation: string 26 | meta: MetaData 27 | } 28 | 29 | interface SendEventToAuditVariables { 30 | input: SendEventToAuditInput 31 | } 32 | 33 | export type SendEventToAuditMutationFn = MutationFn< 34 | SendEventToAuditData, 35 | SendEventToAuditVariables 36 | > 37 | 38 | export interface MutationRenderProps 39 | extends MutationResult { 40 | sendEventToAudit: SendEventToAuditMutationFn 41 | } 42 | 43 | class SendEventToAuditMutation extends Mutation< 44 | SendEventToAuditData, 45 | SendEventToAuditVariables 46 | > { 47 | public static defaultProps = { 48 | mutation: SendEventToAudit, 49 | } 50 | } 51 | 52 | export default SendEventToAuditMutation 53 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/typography/FontFamilyEntry.tsx: -------------------------------------------------------------------------------- 1 | import { useKeydownFromClick } from 'keydown-from-click' 2 | import React from 'react' 3 | import { RouteComponentProps, withRouter } from 'react-router' 4 | 5 | import { FontFamily } from '../queries/ListFontsQuery' 6 | import { EditorPath, IdParam } from '../StyleEditorRouter' 7 | 8 | interface Props extends RouteComponentProps { 9 | font: FontFamily 10 | } 11 | 12 | const FontFamilyEntry: React.FunctionComponent = ({ font, history }) => { 13 | const encodedId = React.useMemo(() => encodeURIComponent(font.id), [font.id]) 14 | 15 | const isActive = React.useMemo(() => document.URL.includes(encodedId), [ 16 | encodedId, 17 | ]) 18 | 19 | const handleClick = React.useCallback( 20 | () => history.push(EditorPath.customFontFile.replace(IdParam, encodedId)), 21 | [encodedId, history] 22 | ) 23 | 24 | const handleKeyDown = useKeydownFromClick(handleClick) 25 | 26 | return ( 27 |
35 | {font.fontFamily} 36 |
37 | ) 38 | } 39 | 40 | export default withRouter(FontFamilyEntry) 41 | -------------------------------------------------------------------------------- /react/components/admin/redirects/Form/Operations.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Mutation } from 'react-apollo' 3 | 4 | import DeleteRedirect from '../../../../queries/DeleteRedirect.graphql' 5 | import SaveRedirect from '../../../../queries/SaveRedirect.graphql' 6 | 7 | import { 8 | DeleteRedirectData, 9 | DeleteRedirectMutationFn, 10 | DeleteRedirectVariables, 11 | SaveRedirectData, 12 | SaveRedirectMutationFn, 13 | SaveRedirectVariables, 14 | } from './typings' 15 | import { getStoreUpdater } from './utils' 16 | 17 | interface Props { 18 | children: (mutations: OperationsObj) => React.ReactNode 19 | } 20 | 21 | interface OperationsObj { 22 | deleteRedirect: DeleteRedirectMutationFn 23 | saveRedirect: SaveRedirectMutationFn 24 | } 25 | 26 | const Operations = (props: Props) => ( 27 | 28 | mutation={DeleteRedirect} 29 | update={getStoreUpdater('delete')} 30 | > 31 | {deleteRedirect => ( 32 | 33 | mutation={SaveRedirect} 34 | update={getStoreUpdater('save')} 35 | > 36 | {saveRedirect => 37 | props.children({ 38 | deleteRedirect, 39 | saveRedirect, 40 | }) 41 | } 42 | 43 | )} 44 | 45 | ) 46 | 47 | export default Operations 48 | -------------------------------------------------------------------------------- /react/components/admin/redirects/Form/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { MutationFn, MutationUpdaterFn } from 'react-apollo' 2 | 3 | import { RedirectsQuery } from '../typings' 4 | 5 | export interface MutationResult { 6 | data?: { 7 | [name: string]: Redirect 8 | } 9 | } 10 | 11 | export type QueryData = RedirectsQuery | null 12 | 13 | export interface RedirectQuery { 14 | redirect: { 15 | get: Redirect 16 | } 17 | } 18 | 19 | export interface DeleteRedirectVariables { 20 | path: string 21 | binding?: string 22 | } 23 | 24 | export interface SaveRedirectVariables { 25 | id?: string 26 | endDate: string | null 27 | from: string 28 | to: string 29 | type: RedirectTypes 30 | binding?: string 31 | } 32 | 33 | interface Mutations { 34 | redirect?: { 35 | delete?: Redirect 36 | save?: Redirect 37 | } 38 | } 39 | 40 | export type StoreUpdaterGetter = ( 41 | operation: 'delete' | 'save' 42 | ) => MutationUpdaterFn 43 | 44 | export interface DeleteRedirectData { 45 | redirect: { 46 | delete: Redirect 47 | } 48 | } 49 | 50 | export type DeleteRedirectMutationFn = MutationFn< 51 | DeleteRedirectData, 52 | DeleteRedirectVariables 53 | > 54 | 55 | export interface SaveRedirectData { 56 | redirect: { 57 | save: Redirect 58 | } 59 | } 60 | 61 | export type SaveRedirectMutationFn = MutationFn< 62 | SaveRedirectData, 63 | SaveRedirectVariables 64 | > 65 | -------------------------------------------------------------------------------- /react/components/HighlightOverlay/hooks/useAutoScroll.ts: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce' 2 | import { useEffect } from 'react' 3 | import { State } from '../typings' 4 | 5 | interface UseAutoScrollArgs { 6 | highlightTreePath: State['highlightTreePath'] 7 | editMode: State['editMode'] 8 | visibleElement?: Element 9 | } 10 | 11 | function isElementInViewport(el: Element) { 12 | const rect = el.getBoundingClientRect() 13 | 14 | return ( 15 | rect.top >= 0 && 16 | rect.left >= 0 && 17 | rect.bottom <= 18 | (window.innerHeight || document.documentElement.clientHeight) && 19 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 20 | ) 21 | } 22 | 23 | const scrollToElement = debounce((element: Element) => { 24 | if (!isElementInViewport(element)) { 25 | element.scrollIntoView({ 26 | behavior: 'smooth', 27 | block: 'center', 28 | inline: 'center', 29 | }) 30 | } 31 | }, 75) 32 | 33 | export default function useAutoScroll({ 34 | highlightTreePath, 35 | editMode, 36 | visibleElement, 37 | }: UseAutoScrollArgs) { 38 | useEffect(() => { 39 | if (visibleElement && !editMode) { 40 | scrollToElement(visibleElement) 41 | } 42 | }, [visibleElement, editMode]) 43 | 44 | useEffect(() => { 45 | if (highlightTreePath === null) { 46 | scrollToElement.cancel() 47 | } 48 | }, [highlightTreePath]) 49 | } 50 | -------------------------------------------------------------------------------- /react/components/admin/utils.ts: -------------------------------------------------------------------------------- 1 | export const slugify = (text: string) => { 2 | const specialChars: { [key: string]: string } = { 3 | à: 'a', 4 | ä: 'a', 5 | á: 'a', 6 | â: 'a', 7 | æ: 'a', 8 | å: 'a', 9 | ë: 'e', 10 | è: 'e', 11 | é: 'e', 12 | ê: 'e', 13 | î: 'i', 14 | ï: 'i', 15 | ì: 'i', 16 | í: 'i', 17 | ò: 'o', 18 | ó: 'o', 19 | ö: 'o', 20 | ô: 'o', 21 | ø: 'o', 22 | ù: 'o', 23 | ú: 'u', 24 | ü: 'u', 25 | û: 'u', 26 | ñ: 'n', 27 | ç: 'c', 28 | ß: 's', 29 | ÿ: 'y', 30 | œ: 'o', 31 | ŕ: 'r', 32 | ś: 's', 33 | ń: 'n', 34 | ṕ: 'p', 35 | ẃ: 'w', 36 | ǵ: 'g', 37 | ǹ: 'n', 38 | ḿ: 'm', 39 | ǘ: 'u', 40 | ẍ: 'x', 41 | ź: 'z', 42 | ḧ: 'h', 43 | '·': '-', 44 | '/': '-', 45 | _: '-', 46 | ',': '-', 47 | ':': '-', 48 | ';': '-', 49 | } 50 | 51 | return text 52 | .toString() 53 | .toLowerCase() 54 | .replace(/\s+/g, '-') 55 | .replace(/./g, target => specialChars[target] || target) // Replace special characters using the hash map 56 | .replace(/&/g, '-and-') // Replace & with 'and' 57 | .replace(/[^\w-]+/g, '') // Remove all non-word chars 58 | .replace(/--+/g, '-') // Replace multiple - with single - 59 | .replace(/^-+/, '') // Trim - from start of text 60 | .replace(/-+$/, '') // Trim - from end of text 61 | } 62 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/ContextSelectors/BindingCloning/utils/initialReducerState.ts: -------------------------------------------------------------------------------- 1 | import { Binding } from '../../typings' 2 | 3 | const formatBindings = (binding: Binding, currentBinding: Binding) => ({ 4 | label: binding.canonicalBaseAddress, 5 | id: binding.id, 6 | supportedLocales: binding.supportedLocales, 7 | checked: false, 8 | overwrites: false, 9 | isCurrent: currentBinding.id === binding.id, 10 | }) 11 | 12 | // Mark items as conflicting or not--that is, if saving on a 13 | // binding will overwrite a page present there or not 14 | export const createInitialCloningState = ( 15 | binding: Binding, 16 | currentBinding: Binding, 17 | routeInfo: Route 18 | ) => { 19 | const formattedBinding = formatBindings(binding, currentBinding) 20 | 21 | if (formattedBinding.isCurrent) { 22 | return formattedBinding 23 | } 24 | 25 | // If the page has undefined binding, it is present on all bindings 26 | if (!routeInfo.binding) { 27 | return { 28 | ...formattedBinding, 29 | overwrites: true, 30 | } 31 | } 32 | 33 | if (!routeInfo.conflicts) { 34 | return formattedBinding 35 | } 36 | 37 | if ( 38 | routeInfo.conflicts.find( 39 | conflict => formattedBinding.id === conflict.binding 40 | ) 41 | ) { 42 | return { 43 | ...formattedBinding, 44 | overwrites: true, 45 | } 46 | } 47 | 48 | return formattedBinding 49 | } 50 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/Card/StatusLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { defineMessages, useIntl } from 'react-intl' 3 | 4 | import ContentActiveIcon from '../../../../../icons/ContentActiveIcon' 5 | import ContentInactiveIcon from '../../../../../icons/ContentInactiveIcon' 6 | import ContentScheduledIcon from '../../../../../icons/ContentScheduledIcon' 7 | 8 | interface Props { 9 | type: 'active' | 'inactive' | 'scheduled' 10 | } 11 | 12 | const messages = defineMessages({ 13 | active: { 14 | defaultMessage: 'Active', 15 | id: 'admin/pages.editor.component-list.status.active', 16 | }, 17 | inactive: { 18 | defaultMessage: 'Inactive', 19 | id: 'admin/pages.editor.component-list.status.inactive', 20 | }, 21 | scheduled: { 22 | defaultMessage: 'Scheduled', 23 | id: 'admin/pages.editor.component-list.status.scheduled', 24 | }, 25 | }) 26 | 27 | const ICON_BY_STATUS = { 28 | active: , 29 | inactive: , 30 | scheduled: , 31 | } 32 | 33 | const StatusLabel: React.FC = ({ type }) => { 34 | const intl = useIntl() 35 | 36 | return ( 37 |
38 | {ICON_BY_STATUS[type]} 39 |
{intl.formatMessage(messages[type])}
40 |
41 | ) 42 | } 43 | 44 | export default StatusLabel 45 | -------------------------------------------------------------------------------- /react/typings/vtex.render-runtime.d.ts: -------------------------------------------------------------------------------- 1 | /* Typings for `render-runtime` */ 2 | declare module 'vtex.render-runtime' { 3 | import { Component, ComponentType, ReactElement } from 'react' 4 | 5 | export const ExtensionPoint: ReactElement 6 | export const Helmet: ComponentType 7 | export const Link: ReactElement 8 | export const NoSSR: ReactElement 9 | export const RenderContextConsumer: ReactElement 10 | export const canUseDOM: boolean 11 | 12 | export const withRuntimeContext: ( 13 | Component: ComponentType 14 | ) => ComponentType< 15 | Pick< 16 | TOriginalProps, 17 | Exclude 18 | > 19 | > 20 | 21 | export declare const useRuntime: () => RenderContext 22 | 23 | interface RenderComponent { 24 | getCustomMessages?: (locale: string) => unknown 25 | schema?: ComponentSchema 26 | getSchema?: (props: object, otherArgs?: unknown) => ComponentSchema 27 | uiSchema?: object 28 | } 29 | 30 | export interface ComponentsRegistry { 31 | [component: string]: RenderComponent 32 | } 33 | 34 | export interface Window extends Window { 35 | __RENDER_8_COMPONENTS__: ComponentsRegistry 36 | } 37 | 38 | export const buildCacheLocator: ( 39 | app: string, 40 | type: string, 41 | cacheId: string 42 | ) => string 43 | 44 | const global: Window 45 | } 46 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { MutationUpdaterFn } from 'react-apollo' 2 | import { ToastConsumerFunctions } from 'vtex.styleguide' 3 | 4 | import { 5 | DeleteContentData, 6 | DeleteContentMutationFn, 7 | } from '../../../mutations/DeleteContent' 8 | import { SendEventToAuditMutationFn } from '../../../mutations/SendEventToAudit' 9 | 10 | interface GetDeleteStoreUpdaterParams 11 | extends Pick { 12 | action: 'reset' | 'delete' 13 | blockId: EditorContextType['blockData']['id'] 14 | iframeRuntime: RenderContext 15 | setBlockData: EditorContextType['setBlockData'] 16 | } 17 | 18 | export type GetDeleteStoreUpdater = ( 19 | params: GetDeleteStoreUpdaterParams 20 | ) => MutationUpdaterFn 21 | 22 | export interface UseListHandlersParams { 23 | activeContentId: ExtensionConfiguration['contentId'] 24 | deleteContent: DeleteContentMutationFn 25 | sendEventToAudit: SendEventToAuditMutationFn 26 | iframeRuntime: RenderContext 27 | intl: ReactIntl.InjectedIntl 28 | showToast: ToastConsumerFunctions['showToast'] 29 | } 30 | 31 | export type UseListHandlers = ( 32 | params: UseListHandlersParams 33 | ) => { 34 | handleConfirmConfigurationDelete: ( 35 | configuration: ExtensionConfiguration 36 | ) => void 37 | handleConfigurationDelete: ( 38 | configuration: ExtensionConfiguration 39 | ) => Promise 40 | } 41 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/typography/TypeTokenDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch } from 'react' 2 | 3 | import startCase from 'lodash/startCase' 4 | import { Dropdown } from 'vtex.styleguide' 5 | 6 | import { FontFamily } from '../queries/ListFontsQuery' 7 | import { getTypeTokenDropdownOptions } from '../utils/typography' 8 | 9 | function getOnChange(key: keyof Font, dispatch: Dispatch>) { 10 | return (_: React.ChangeEvent, value: string) => { 11 | // Workaround since Dropdown does not work well with nil values 12 | dispatch({ [key]: value === '' ? null : value }) 13 | } 14 | } 15 | 16 | function getValue(key: keyof Font, font: Font): string { 17 | return font[key] || '' 18 | } 19 | 20 | interface EntryProps { 21 | font: Font 22 | fontFamilies: FontFamily[] 23 | id: keyof Font 24 | dispatch: Dispatch> 25 | } 26 | 27 | const TypeTokenDropdown: React.FunctionComponent = ({ 28 | font, 29 | id, 30 | dispatch, 31 | fontFamilies, 32 | }) => { 33 | return ( 34 |
35 | {startCase(id)} 36 | 42 |
43 | ) 44 | } 45 | 46 | export default TypeTokenDropdown 47 | -------------------------------------------------------------------------------- /react/components/form/ArrayFieldTemplate/styles.css: -------------------------------------------------------------------------------- 1 | .accordion-item { 2 | background-color: white; 3 | } 4 | 5 | .accordion-item-settings { 6 | padding: 2rem; 7 | transform: translateX(0rem) !important; 8 | } 9 | 10 | .accordion-list-container--sorting .accordion-item { 11 | pointer-events: none; 12 | } 13 | 14 | .accordion-item--dragged { 15 | z-index: 999; 16 | } 17 | 18 | .accordion-label { 19 | display: flex; 20 | position: relative; 21 | margin: 0 -24px 0; 22 | padding: 12px 16px; 23 | cursor: pointer; 24 | user-select: none; 25 | } 26 | 27 | .accordion-item--dragged .accordion-label { 28 | background-color: #fff; 29 | } 30 | 31 | .accordion-item--handle-hidden .accordion-label { 32 | padding-left: 16px; 33 | } 34 | 35 | .accordion-item--dragged .accordion-label { 36 | box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); 37 | } 38 | 39 | .accordion-label-title { 40 | color: #727273; 41 | font-weight: bold; 42 | cursor: pointer; 43 | } 44 | 45 | .accordion-item--dragged .accordion-handle, 46 | .accordion-label:hover .accordion-handle { 47 | display: block; 48 | } 49 | 50 | .drag-handle-container { 51 | transform: translateZ(0); 52 | transition: transform ease-in-out 100ms; 53 | } 54 | 55 | .drag-handle-container:hover, 56 | .accordion-item--dragged .drag-handle-container { 57 | transform: scale(1.1); 58 | } 59 | 60 | .drag-handle-container:hover circle, 61 | .accordion-item--dragged .drag-handle-container circle { 62 | fill: #368df7; 63 | } 64 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createMemoryHistory } from 'history' 3 | import { injectIntl, IntlShape } from 'react-intl' 4 | import { Router } from 'react-router-dom' 5 | import { ToastConsumer } from 'vtex.styleguide' 6 | 7 | import RenameStyleMutation from './mutations/RenameStyle' 8 | import UpdateStyleMutation from './mutations/UpdateStyle' 9 | import StyleEditorHooks from './StyleEditorHooks' 10 | 11 | interface Props { 12 | intl: IntlShape 13 | setStyleAsset: (asset: StyleAssetInfo) => void 14 | stopEditing: () => void 15 | style: Style 16 | } 17 | 18 | const StyleEditor: React.FunctionComponent = props => { 19 | return ( 20 | 21 | 22 | {renameStyle => ( 23 | 24 | {updateStyle => ( 25 | 26 | {({ showToast }) => ( 27 | 35 | )} 36 | 37 | )} 38 | 39 | )} 40 | 41 | 42 | ) 43 | } 44 | 45 | export default injectIntl(StyleEditor) 46 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/DeviceItem.tsx: -------------------------------------------------------------------------------- 1 | import { createKeydownFromClick } from 'keydown-from-click' 2 | import React from 'react' 3 | import { useIntl } from 'react-intl' 4 | import { Tooltip } from 'vtex.styleguide' 5 | 6 | import { useHover } from '../hooks' 7 | import { BORDER_BY_POSITION, ICON_BY_VIEWPORT } from './consts' 8 | import messages from './messages' 9 | 10 | interface Props { 11 | isActive: boolean 12 | onClick: (e: Pick) => void 13 | position: keyof typeof BORDER_BY_POSITION 14 | type: Viewport 15 | } 16 | 17 | const DeviceItem: React.FC = ({ onClick, position, isActive, type }) => { 18 | const handleKeyDown = createKeydownFromClick(onClick) 19 | 20 | const { handleMouseEnter, handleMouseLeave, hover } = useHover() 21 | const intl = useIntl() 22 | 23 | const Icon = ICON_BY_VIEWPORT[type] 24 | 25 | return ( 26 | 27 | 39 | 40 | ) 41 | } 42 | 43 | export default DeviceItem 44 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/DeviceSwitcher/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useEditorContext } from '../../../EditorContext' 4 | 5 | import { VIEWPORTS_BY_DEVICE } from './consts' 6 | import DeviceItem from './DeviceItem' 7 | 8 | import styles from './DeviceSwitcher.css' 9 | 10 | interface Props { 11 | device: RenderContext['device'] 12 | } 13 | 14 | const DeviceSwitcher: React.FC = ({ device }) => { 15 | const editor = useEditorContext() 16 | 17 | const handleClick = React.useCallback( 18 | ({ currentTarget }: Pick) => { 19 | if (currentTarget && currentTarget instanceof HTMLElement) { 20 | editor.setViewport(currentTarget.dataset.type as Viewport) 21 | } 22 | }, 23 | [editor] 24 | ) 25 | 26 | const viewports = VIEWPORTS_BY_DEVICE[device === 'any' ? 'desktop' : device] 27 | 28 | return ( 29 |
30 | {viewports.map((deviceType, index) => { 31 | const isLast = index === viewports.length - 1 32 | 33 | return ( 34 |
35 | 41 |
42 | ) 43 | })} 44 |
45 | ) 46 | } 47 | 48 | export default DeviceSwitcher 49 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockSelector/BlockList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useEditorContext } from '../../../../EditorContext' 4 | import { NormalizedBlock } from '../typings' 5 | import BlockListItem from './BlockListItem' 6 | 7 | interface Props { 8 | blocks: NormalizedBlock[] 9 | highlightHandler: (treePath: string | null) => void 10 | iframeRuntime: RenderContextProps['runtime'] 11 | onMouseEnterBlock: ( 12 | event: React.MouseEvent 13 | ) => void 14 | onMouseLeaveBlock: () => void 15 | } 16 | 17 | const BlockList: React.FC = ({ 18 | blocks, 19 | highlightHandler, 20 | onMouseEnterBlock, 21 | onMouseLeaveBlock, 22 | }) => { 23 | const editor = useEditorContext() 24 | 25 | const handleEdit = React.useCallback( 26 | (block: NormalizedBlock) => { 27 | if (block.isEditable) { 28 | editor.editExtensionPoint(block.treePath) 29 | 30 | editor.setIsLoading(true) 31 | 32 | highlightHandler(null) 33 | } 34 | }, 35 | [editor, highlightHandler] 36 | ) 37 | 38 | return ( 39 |
    40 | {blocks.map((block, index) => ( 41 | 48 | ))} 49 |
50 | ) 51 | } 52 | 53 | export default BlockList 54 | -------------------------------------------------------------------------------- /react/components/form/ImageUploader/Dropzone.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import ReactDropzone, { DropzoneOptions, useDropzone } from 'react-dropzone' 3 | 4 | interface Props { 5 | children: ReactElement 6 | disabled: boolean 7 | extraClasses?: string 8 | onClick: React.MouseEventHandler 9 | onDrop: DropzoneOptions['onDrop'] 10 | ref?: React.Ref 11 | } 12 | 13 | const MAX_SIZE = 4 * 1024 * 1024 14 | 15 | const Dropzone = React.forwardRef( 16 | ({ disabled, children, extraClasses, onClick, onDrop }, ref) => { 17 | const { isDragActive } = useDropzone() 18 | 19 | const stlye = isDragActive 20 | ? { borderColor: '#134cd8', width: '14.25rem', height: '7rem' } 21 | : {} 22 | 23 | return ( 24 | 31 | {({ getRootProps, getInputProps }) => ( 32 |
onClick(e), stlye })} 34 | className={`w-100 h4 br2 ${extraClasses}`} 35 | ref={ref} 36 | > 37 | 38 | {children} 39 |
40 | )} 41 |
42 | ) 43 | } 44 | ) 45 | 46 | Dropzone.displayName = 'Dropzone' 47 | 48 | Dropzone.defaultProps = { 49 | extraClasses: '', 50 | } 51 | 52 | export default Dropzone 53 | -------------------------------------------------------------------------------- /react/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Modal as StyleguideModal } from 'vtex.styleguide' 3 | 4 | interface Props { 5 | isActionDanger?: boolean 6 | isActionLoading: boolean 7 | isOpen: boolean 8 | onClickAction?: (event: Event) => void 9 | onClickCancel?: (event: Event) => void 10 | onClose: (event?: Event) => void 11 | textButtonAction: string 12 | textButtonCancel: string 13 | textMessage: string | React.ReactNode 14 | title?: string 15 | } 16 | 17 | const Modal = ({ 18 | isActionDanger = false, 19 | isActionLoading, 20 | isOpen, 21 | onClickAction, 22 | onClickCancel, 23 | onClose, 24 | textButtonAction, 25 | textButtonCancel, 26 | textMessage, 27 | title, 28 | }: Props) => ( 29 | 30 |

{textMessage}

31 | 32 |
33 |
34 | 42 |
43 | 44 | 52 |
53 |
54 | ) 55 | 56 | export default Modal 57 | -------------------------------------------------------------------------------- /react/components/admin/pages/Form/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { ConditionsProps } from 'vtex.styleguide' 2 | 3 | export interface DeleteMutationResult { 4 | data?: { 5 | deleteRoute: string 6 | } 7 | } 8 | 9 | export interface SaveMutationResult { 10 | data?: { 11 | saveRoute: Route 12 | } 13 | } 14 | 15 | export interface TemplateMutationResult { 16 | data?: { 17 | availableTemplates: Template[] 18 | } 19 | } 20 | 21 | export type QueryData = RoutesQuery | null 22 | 23 | export interface RoutesQuery { 24 | routes: Route[] 25 | } 26 | 27 | export interface ClientSideUniqueId { 28 | uniqueId: number 29 | operator: ConditionsProps['operator'] 30 | } 31 | 32 | interface StatementsOnSaveRoute { 33 | subject: string 34 | verb: string 35 | objectJSON: string 36 | } 37 | 38 | interface ConditionsOnSaveRouteVariables { 39 | id?: string 40 | allMatches: boolean 41 | statements: StatementsOnSaveRoute[] 42 | } 43 | 44 | export interface SaveRouteVariables { 45 | route: Partial & 46 | Pick 47 | } 48 | 49 | export interface DeleteRouteVariables { 50 | uuid: string 51 | } 52 | 53 | export interface DateInfoFormat { 54 | date: string 55 | to: string 56 | from: string 57 | } 58 | 59 | export type DateStatementFormat = Record 60 | 61 | export type FormErrors = Omit, 'pages'> & { 62 | pages?: { 63 | [key: string]: { 64 | template?: string 65 | condition?: string 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /react/components/form/ImageUploader/ImagePreview.tsx: -------------------------------------------------------------------------------- 1 | import { createKeydownFromClick } from 'keydown-from-click' 2 | import React, { useMemo } from 'react' 3 | 4 | import styles from './ImagePreview.css' 5 | 6 | interface Props { 7 | imageUrl: string 8 | children: React.ReactElement 9 | } 10 | 11 | function stopPropagation( 12 | e: Pick, 'stopPropagation'> 13 | ) { 14 | e.stopPropagation() 15 | } 16 | 17 | const stopOnKeyDownPropagation = createKeydownFromClick(stopPropagation) 18 | 19 | const ImagePreview: React.FC = ({ children, imageUrl }) => { 20 | const backgroundImageStyle = useMemo( 21 | () => ({ 22 | backgroundImage: `url(${imageUrl}?width=710&height=384&aspect=true)`, 23 | }), 24 | [imageUrl] 25 | ) 26 | 27 | return ( 28 |
32 |
35 |
43 | {children} 44 |
45 |
46 |
47 | ) 48 | } 49 | 50 | export default React.memo(ImagePreview) 51 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What problem is this solving? 2 | 3 | 4 | 5 | #### How should this be manually tested? 6 | 7 | [Workspace](url) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | #### Screenshots or example usage 18 | 19 | #### Type of changes 20 | 21 | 22 | 23 | | ✔️ | Type of Change | 24 | | --- | ----------------------------------------------------------------------------------------- | 25 | | \_ | Bug fix | 26 | | \_ | New feature | 27 | | \_ | Breaking change | 28 | | \_ | Technical improvements | 29 | 30 | #### Notes 31 | 32 | 33 | 34 | #### How does this PR make you feel? [:link:](http://giphy.com/categories/emotions/) 35 | 36 | ![](put .gif link here - can be found under "advanced" on giphy) 37 | -------------------------------------------------------------------------------- /react/components/icons/GearIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const GearIcon: React.FunctionComponent = () => ( 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default GearIcon 20 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/utils.ts: -------------------------------------------------------------------------------- 1 | import throttle from 'lodash/throttle' 2 | 3 | import { updateExtensionFromForm } from '../../../../utils/components' 4 | import { NEW_CONFIGURATION_ID } from '../consts' 5 | import { getDefaultCondition } from '../utils' 6 | import { GetDefaultConfiguration, GetConfigurationType } from './typings' 7 | 8 | export const getDefaultConfiguration: GetDefaultConfiguration = ({ 9 | iframeRuntime, 10 | isSitewide, 11 | }) => ({ 12 | condition: getDefaultCondition({ iframeRuntime, isSitewide }), 13 | contentId: NEW_CONFIGURATION_ID, 14 | contentJSON: '{}', 15 | label: null, 16 | origin: null, 17 | }) 18 | 19 | export const getIsDefaultContent: ( 20 | configuration: Pick 21 | ) => boolean = configuration => configuration.origin !== null 22 | 23 | export const omitUndefined = (obj: Extension['content']) => 24 | Object.entries(obj).reduce((acc, [currKey, currValue]) => { 25 | if (typeof currValue === 'undefined') { 26 | return acc 27 | } 28 | 29 | return { ...acc, [currKey]: currValue } 30 | }, {}) 31 | 32 | export const getConfigurationType: GetConfigurationType = ({ 33 | configuration, 34 | activeContentId, 35 | }) => { 36 | if (getIsDefaultContent(configuration)) { 37 | return 'app' 38 | } 39 | 40 | if (activeContentId === configuration.contentId) { 41 | return 'active' 42 | } 43 | 44 | return 'inactive' 45 | } 46 | 47 | export const throttledUpdateExtensionFromForm = throttle( 48 | data => updateExtensionFromForm(data), 49 | 200 50 | ) 51 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/FormMetaContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createContext, useContext } from 'react' 2 | 3 | import { FormMetaContext as FormMetaContextT } from './typings' 4 | 5 | const defaultExternalState: FormMetaContextT = { 6 | getWasModified: () => { 7 | return false 8 | }, 9 | setWasModified: () => { 10 | return 11 | }, 12 | } 13 | 14 | const FormMetaContext = createContext(defaultExternalState) 15 | 16 | export const useFormMetaContext = () => useContext(FormMetaContext) 17 | 18 | export const FormMetaConsumer = FormMetaContext.Consumer 19 | 20 | interface State extends FormMetaContextT { 21 | isLoading: boolean 22 | wasModified: boolean 23 | } 24 | 25 | export class FormMetaProvider extends Component<{}, State> { 26 | public constructor(props: {}) { 27 | super(props) 28 | 29 | this.state = { 30 | ...defaultExternalState, 31 | getWasModified: this.getWasModified, 32 | isLoading: false, 33 | setWasModified: this.setWasModified, 34 | wasModified: false, 35 | } 36 | } 37 | 38 | public render() { 39 | return ( 40 | 41 | {this.props.children} 42 | 43 | ) 44 | } 45 | 46 | private getWasModified: State['getWasModified'] = () => this.state.wasModified 47 | 48 | private setWasModified: State['setWasModified'] = (newValue, callback) => { 49 | this.setState({ wasModified: newValue }, () => { 50 | if (callback) { 51 | callback() 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendor": "vtex", 3 | "name": "admin-pages", 4 | "version": "4.59.0", 5 | "title": "VTEX Pages Admin", 6 | "description": "The VTEX Pages CMS admin interface", 7 | "builders": { 8 | "admin": "0.x", 9 | "messages": "1.x", 10 | "react": "3.x", 11 | "docs": "0.x" 12 | }, 13 | "dependencies": { 14 | "vtex.apps-graphql": "3.x", 15 | "vtex.file-manager": "0.x", 16 | "vtex.file-manager-graphql": "0.x", 17 | "vtex.native-types": "0.x", 18 | "vtex.pages-graphql": "2.x", 19 | "vtex.pwa-graphql": "1.x", 20 | "vtex.store": "2.x", 21 | "vtex.styles-graphql": "1.x", 22 | "vtex.styleguide": "9.x", 23 | "vtex.messages": "1.x", 24 | "vtex.rewriter": "1.x", 25 | "vtex.tenant-graphql": "0.x", 26 | "vtex.admin-cms": "1.x" 27 | }, 28 | "mustUpdateAt": "2018-09-05", 29 | "categories": [], 30 | "registries": [ 31 | "smartcheckout" 32 | ], 33 | "settingsSchema": { 34 | "title": "VTEX Pages Admin", 35 | "type": "object", 36 | "properties": { 37 | "copyContentBinding": { 38 | "type": "boolean", 39 | "title": "Copy Content Binding", 40 | "description": "Enables feature to copy pages from one binding to another", 41 | "default": false 42 | } 43 | } 44 | }, 45 | "scripts": { 46 | "postreleasy": "vtex publish -r vtex --verbose" 47 | }, 48 | "policies": [ 49 | { 50 | "name": "vtex.file-manager:saveFileAsync" 51 | } 52 | ], 53 | "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema" 54 | } 55 | -------------------------------------------------------------------------------- /react/components/form/MultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import ReactSelect, { Option } from 'react-select' 3 | 4 | interface Props { 5 | disabled?: boolean 6 | id?: string 7 | label?: string 8 | onChange: (newValue: string[]) => void 9 | options: { 10 | enumOptions: Option[] 11 | } 12 | placeholder: string 13 | schema?: { 14 | disabled?: boolean 15 | title: string 16 | } 17 | value: string[] 18 | } 19 | 20 | const MultiSelect: React.FunctionComponent = ({ 21 | disabled, 22 | id, 23 | label, 24 | onChange, 25 | options: { enumOptions }, 26 | placeholder, 27 | schema, 28 | value, 29 | }) => ( 30 | 31 | {(label || (schema && schema.title)) && ( 32 | 37 | )} 38 | { 45 | const formattedValue = (optionValues as Option[]).map( 46 | (item: Option) => item.value as string 47 | ) 48 | onChange(formattedValue) 49 | }} 50 | options={enumOptions} 51 | placeholder={placeholder} 52 | value={value} 53 | /> 54 | 55 | ) 56 | 57 | MultiSelect.defaultProps = { 58 | disabled: false, 59 | placeholder: '', 60 | value: [], 61 | } 62 | 63 | export default MultiSelect 64 | -------------------------------------------------------------------------------- /react/components/admin/redirects/UploadModal/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { Button, Progress, Spinner } from 'vtex.styleguide' 4 | 5 | interface Props { 6 | current: number 7 | total: number 8 | onCancel: () => void 9 | } 10 | 11 | const Loading: React.FunctionComponent = ({ 12 | current, 13 | onCancel, 14 | total, 15 | }) => { 16 | const percent = 17 | Math.round((current * 100) / total) < 100 18 | ? Math.round((current * 100) / total) 19 | : 100 20 | 21 | return ( 22 |
23 |
24 | 25 |
26 |
27 | {`${percent}%`} 28 | 29 | {current + ' '}/{' ' + total} 30 | 31 |
32 | 33 |
34 | 38 | 44 |
45 |
46 | ) 47 | } 48 | 49 | export default Loading 50 | -------------------------------------------------------------------------------- /react/components/admin/redirects/bulkUploadRedirects.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject } from 'react' 2 | import { splitEvery } from 'ramda' 3 | 4 | const MAX_REDIRECTS_PER_REQUEST = 200 5 | const NUMBER_OF_RETRIES = 3 6 | 7 | interface BulkUploadRedirectsArgs { 8 | data: Redirect[] 9 | mutation: (data: Redirect[]) => Promise 10 | isSave: boolean 11 | shouldUploadRef: MutableRefObject 12 | updateProgress?: (processed: number) => void 13 | } 14 | 15 | export default async function bulkUploadRedirects({ 16 | data, 17 | mutation, 18 | shouldUploadRef, 19 | updateProgress = () => { 20 | return undefined 21 | }, 22 | }: BulkUploadRedirectsArgs): Promise<{ failedRedirects: Redirect[] }> { 23 | let failedRedirects: Redirect[] = [] 24 | let processedCount = 0 25 | 26 | const redirectBatches = splitEvery(MAX_REDIRECTS_PER_REQUEST, data) 27 | 28 | for (const payload of redirectBatches) { 29 | if (!shouldUploadRef.current) { 30 | break 31 | } 32 | 33 | for (let i = 1; i <= NUMBER_OF_RETRIES; i++) { 34 | try { 35 | await mutation(payload) 36 | processedCount += payload.length 37 | updateProgress(processedCount) 38 | break 39 | } catch (e) { 40 | await new Promise(res => { 41 | setTimeout(() => res(), i * 750) 42 | }) 43 | if (i === NUMBER_OF_RETRIES) { 44 | failedRedirects = failedRedirects.concat(payload) 45 | } 46 | } 47 | } 48 | 49 | await new Promise(res => { 50 | setTimeout(() => res(), 750) 51 | }) 52 | } 53 | 54 | return { failedRedirects } 55 | } 56 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/styles.css: -------------------------------------------------------------------------------- 1 | .transition-editor-enter { 2 | transform: translateX(18em); 3 | } 4 | 5 | .transition-editor-enter-active { 6 | transition: transform 250ms ease-in; 7 | transform: translateX(0); 8 | } 9 | 10 | .transition-editor-enter-done { 11 | transform: translateX(0); 12 | } 13 | 14 | .transition-editor-exit { 15 | transform: translateX(0); 16 | } 17 | 18 | .transition-editor-exit-active { 19 | transition: transform 250ms ease-in; 20 | transform: translateX(18em); 21 | } 22 | 23 | .transition-editor-exit-done { 24 | transform: translateX(18em); 25 | } 26 | 27 | .transition-selector-enter { 28 | transform: translateX(0); 29 | } 30 | 31 | .transition-selector-enter-active { 32 | transition: transform 250ms ease-in; 33 | transform: translateX(-18em); 34 | } 35 | 36 | .transition-selector-enter-done { 37 | transform: translateX(-18em); 38 | } 39 | 40 | .transition-selector-exit { 41 | transform: translateX(-18em); 42 | } 43 | 44 | .transition-selector-exit-active { 45 | transition: transform 250ms ease-in; 46 | transform: translateX(0); 47 | } 48 | 49 | .transition-selector-exit-done { 50 | transform: translateX(0); 51 | } 52 | 53 | /* Non small */ 54 | @media screen and (min-width: 30em) { 55 | .admin-sidebar { 56 | border-left: 2px solid #f2f4f5; 57 | } 58 | 59 | .admin-sidebar::-webkit-scrollbar, 60 | .admin-sidebar::-webkit-scrollbar-track { 61 | width: 2px; 62 | background: #f2f4f5; 63 | } 64 | 65 | .admin-sidebar::-webkit-scrollbar-thumb { 66 | width: 2px; 67 | background: rgba(0, 0, 0, 0.2); 68 | border-radius: 2px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/SidebarVisibilityToggle.tsx: -------------------------------------------------------------------------------- 1 | import { createKeydownFromClick } from 'keydown-from-click' 2 | import React from 'react' 3 | import { InjectedIntlProps, injectIntl } from 'react-intl' 4 | import { Tooltip } from 'vtex.styleguide' 5 | 6 | import { useEditorContext } from '../../EditorContext' 7 | 8 | import { useHover } from './hooks' 9 | import IconView from './icons/IconView' 10 | 11 | const SidebarVisibilityToggle: React.FC = ({ intl }) => { 12 | const editor = useEditorContext() 13 | 14 | const { handleMouseEnter, handleMouseLeave, hover } = useHover() 15 | 16 | const handleSidebarVisibilityToggle = editor.toggleSidebarVisibility 17 | 18 | const handleSidebarVisibilityToggleKeyDown = createKeydownFromClick( 19 | handleSidebarVisibilityToggle 20 | ) 21 | 22 | return ( 23 | 30 | 43 | 44 | ) 45 | } 46 | 47 | export default injectIntl(SidebarVisibilityToggle) 48 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationEditor/ConditionControls/ScopeSelector/ScopeSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { InjectedIntlProps, injectIntl } from 'react-intl' 3 | import { RadioGroup } from 'vtex.styleguide' 4 | 5 | import ConditionTitle from '../ConditionTitle' 6 | import { getScopeStandardOptions } from './utils' 7 | 8 | interface CustomProps { 9 | onChange: (event: React.ChangeEvent) => void 10 | pageContext: PageContext 11 | scope: ConfigurationScope 12 | isSitewide: boolean 13 | } 14 | 15 | type Props = CustomProps & InjectedIntlProps 16 | 17 | const ScopeSelector: React.FunctionComponent = ({ 18 | intl, 19 | isSitewide, 20 | onChange, 21 | pageContext, 22 | scope, 23 | }) => { 24 | const standardOptions = getScopeStandardOptions(intl, pageContext) 25 | 26 | return ( 27 | 28 | 29 | 30 | 50 | 51 | ) 52 | } 53 | 54 | export default injectIntl(ScopeSelector) 55 | -------------------------------------------------------------------------------- /react/components/admin/pages/List/Section.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { withRuntimeContext } from 'vtex.render-runtime' 4 | import { Box, Button } from 'vtex.styleguide' 5 | 6 | import { NEW_ROUTE_ID, ROUTES_FORM } from '../consts' 7 | import SeparatorWithLine from '../SeparatorWithLine' 8 | 9 | import Entry from './Entry' 10 | 11 | interface Props { 12 | hasCreateButton?: boolean 13 | routes: Route[] 14 | runtime: RenderContext 15 | titleId: string 16 | } 17 | 18 | const Section = ({ hasCreateButton, routes, runtime, titleId }: Props) => ( 19 | 20 |
21 | 22 | {title =>
{title}
} 23 |
24 | {hasCreateButton && ( 25 |
26 | 41 |
42 | )} 43 |
44 | {routes.map(route => ( 45 | 46 | 47 | 48 | 49 | ))} 50 |
51 | ) 52 | 53 | export default withRuntimeContext(Section) 54 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Styles/StyleEditor/mutations/DeleteFontFamily.ts: -------------------------------------------------------------------------------- 1 | import { DataProxy } from 'apollo-cache' 2 | import { Mutation, MutationFn, QueryResult } from 'react-apollo' 3 | 4 | import DeleteFontFamilyMutation from '../graphql/DeleteFontFamily.graphql' 5 | import ListFonts from '../graphql/ListFonts.graphql' 6 | import { ListFontsData } from '../queries/ListFontsQuery' 7 | 8 | interface FontFamilyVariables { 9 | id: string 10 | } 11 | 12 | export interface DeleteFontFamilyData { 13 | deleteFontFamily: { 14 | id: string 15 | } 16 | } 17 | 18 | export type DeleteFontFamilyFn = MutationFn< 19 | DeleteFontFamilyData, 20 | FontFamilyVariables 21 | > 22 | 23 | type DeleteFontFamilyResult = QueryResult 24 | 25 | const updateFontsAfterDelete = ( 26 | cache: DataProxy, 27 | result: DeleteFontFamilyResult 28 | ) => { 29 | const listData = cache.readQuery({ query: ListFonts }) 30 | if ( 31 | result.data == null || 32 | result.data.deleteFontFamily == null || 33 | listData == null 34 | ) { 35 | return 36 | } 37 | const { listFonts: families } = listData 38 | const { id: removedId } = result.data.deleteFontFamily 39 | const currentFonts = families.filter(family => family.id !== removedId) 40 | cache.writeQuery({ 41 | data: { listFonts: currentFonts }, 42 | query: ListFonts, 43 | }) 44 | } 45 | 46 | class DeleteFontFamily extends Mutation< 47 | DeleteFontFamilyData, 48 | FontFamilyVariables 49 | > { 50 | public static defaultProps = { 51 | mutation: DeleteFontFamilyMutation, 52 | update: updateFontsAfterDelete, 53 | } 54 | } 55 | 56 | export default DeleteFontFamily 57 | -------------------------------------------------------------------------------- /react/components/EditorContainer/Topbar/BlockPicker.tsx: -------------------------------------------------------------------------------- 1 | import { useKeydownFromClick } from 'keydown-from-click' 2 | import React from 'react' 3 | import { InjectedIntlProps, injectIntl } from 'react-intl' 4 | import { Tooltip } from 'vtex.styleguide' 5 | 6 | import { useEditorContext } from '../../EditorContext' 7 | import { useHover } from './hooks' 8 | import IconPicker from './icons/IconPicker' 9 | 10 | const BlockPicker: React.FC = ({ intl }) => { 11 | const editor = useEditorContext() 12 | 13 | const { handleMouseEnter, handleMouseLeave, hover } = useHover() 14 | 15 | const handleEditModeToggle = editor.toggleEditMode 16 | 17 | const handleKeyPress = useKeydownFromClick(handleEditModeToggle) 18 | 19 | return ( 20 | 27 | 42 | 43 | ) 44 | } 45 | 46 | export default injectIntl(BlockPicker) 47 | -------------------------------------------------------------------------------- /react/PageEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useRuntime } from 'vtex.render-runtime' 3 | 4 | import StoreIframe from './components/EditorContainer/StoreIframe' 5 | import EditorProvider from './components/EditorProvider' 6 | import { TemporaryAlert } from './components/TemporaryAlert' 7 | import { useAdminLoadingContext } from './utils/AdminLoadingContext' 8 | 9 | interface Props extends RenderContextProps { 10 | page: string 11 | params: { 12 | targetPath: string 13 | } 14 | query?: Record[] 15 | } 16 | 17 | const PageEditor: React.FC = props => { 18 | const { page, params, query } = props 19 | 20 | const queryString = 21 | query === undefined || query === null 22 | ? '' 23 | : `?${Object.entries(query) 24 | .map(([key, value]) => `${key}=${value}`) 25 | .join('&')}` 26 | 27 | const runtime = useRuntime() 28 | 29 | if (page.includes('storefront')) { 30 | runtime.navigate({ 31 | page: page.replace('storefront', 'site-editor'), 32 | params, 33 | }) 34 | } 35 | 36 | const isSiteEditor = React.useMemo(() => page.includes('site-editor'), [page]) 37 | 38 | const path = params?.targetPath && `${params.targetPath}${queryString ?? ''}` 39 | 40 | const { stopLoading } = useAdminLoadingContext() 41 | 42 | useEffect(() => { 43 | stopLoading() 44 | }, [stopLoading]) 45 | 46 | return ( 47 |
48 | 49 | 50 | 51 | 52 |
53 | ) 54 | } 55 | 56 | export default PageEditor 57 | -------------------------------------------------------------------------------- /react/components/HighlightOverlay/hooks/useStyles/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react' 2 | 3 | import { UseStyles } from './typings' 4 | import { getStyles } from './utils' 5 | 6 | /* 7 | * The "data-extension-point" attribute is re-added to the element every 8 | * time it's edited. By observing it, we can consistently check if the 9 | * block's automatic height has changed and adapt the HighlightOverlay's 10 | * height accordingly. 11 | */ 12 | 13 | const useStyles: UseStyles = ({ 14 | hasValidElement, 15 | highlightTreePath, 16 | isOverlayMaskActive, 17 | setState, 18 | subscribeToResize, 19 | unsubscribeToResize, 20 | visibleElement, 21 | }) => { 22 | const updateStyles = useCallback( 23 | (clientHeight?) => { 24 | const elementHeight: HTMLElement['clientHeight'] = clientHeight 25 | 26 | const styles = getStyles({ 27 | hasValidElement, 28 | highlightTreePath, 29 | visibleElement, 30 | }) 31 | 32 | setState(state => ({ 33 | ...state, 34 | ...styles, 35 | elementHeight: elementHeight || state.elementHeight, 36 | })) 37 | }, 38 | [hasValidElement, highlightTreePath, setState, visibleElement] 39 | ) 40 | 41 | useEffect(() => { 42 | function resizeCallback(element: Element) { 43 | updateStyles(element.clientHeight) 44 | } 45 | 46 | subscribeToResize(resizeCallback) 47 | updateStyles() 48 | 49 | return () => { 50 | unsubscribeToResize(resizeCallback) 51 | } 52 | }, [ 53 | isOverlayMaskActive, 54 | subscribeToResize, 55 | unsubscribeToResize, 56 | updateStyles, 57 | visibleElement, 58 | ]) 59 | } 60 | 61 | export default useStyles 62 | --------------------------------------------------------------------------------