configured typescript strict mode in the frontend

This commit is contained in:
Dmitri 2026-07-06 21:19:39 +02:00
parent 2d63d2db6d
commit c02a826aa5
500 changed files with 2798 additions and 2834 deletions

View File

@ -35,15 +35,17 @@ Frontend:
Цель: включить strict TypeScript как обязательный стандарт для frontend и закрыть legacy errors без сохранения JS/CommonJS как допустимого направления. Новый и изменённый код должен быть strict-compatible. Цель: включить strict TypeScript как обязательный стандарт для frontend и закрыть legacy errors без сохранения JS/CommonJS как допустимого направления. Новый и изменённый код должен быть strict-compatible.
Статус: выполнено 2026-07-06 для strict TypeScript baseline и frontend lint-warning cleanup. Первичный запуск `strict: true` давал 294 ошибки; после исправлений `frontend/tsconfig.json` использует общий `strict: true`, `npm run typecheck` проходит, `npm run lint` проходит без warnings, `npm run test` проходит (307 tests), `npm run test:e2e` проходит (10 tests), `npm run build` проходит.
TODO: TODO:
- Посчитать текущий frontend typecheck baseline: сколько ошибок даёт `strict: true`. - [x] Посчитать текущий frontend typecheck baseline: сколько ошибок даёт `strict: true`.
- Включать strictness по шагам: сначала отдельный strict config для selected folders/new code, затем общий `strict: true`, когда baseline закрыт. - [x] Включать strictness по шагам: сначала отдельный strict config для selected folders/new code, затем общий `strict: true`, когда baseline закрыт.
- Для новых/изменённых файлов запрещать новый `any` без явной причины. - [x] Для новых/изменённых файлов запрещать новый `any` без явной причины через `@typescript-eslint/no-explicit-any` warning.
- Запретить новые type assertions/casts (`as`, angle-bracket assertions, non-null `!`) в feature code. Исключения: validated external boundaries, DOM/library interop, discriminated unions после guard; исключение должно быть локальным и объяснённым. - [x] Вернуть guardrails для type assertions/casts (`as`, angle-bracket assertions, non-null `!`) как warnings. Исключения: validated external boundaries, DOM/library interop, discriminated unions после guard; исключение должно быть локальным и объяснённым.
- Вернуть `no-unused-vars`/unused imports как warning, затем поднять до error после cleanup. - [x] Вернуть `no-unused-vars`/unused imports как warning и очистить текущие warnings.
- Добавить frontend `typecheck` script и включить его в minimal checks, когда baseline проходит. - [x] Добавить frontend `typecheck` script и включить его в minimal checks, когда baseline проходит.
- `react-hooks/exhaustive-deps` включать file-by-file после исправления конкретных hooks. - [x] Вернуть `react-hooks/exhaustive-deps` как warning и очистить текущие hook dependency warnings.
### Auth storage ### Auth storage

View File

@ -14,10 +14,26 @@ module.exports = {
}, },
rules: { rules: {
'react/no-children-prop': 'off', 'react/no-children-prop': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'off', // Turned off to reduce noise '@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/consistent-type-assertions': [
'warn',
{
assertionStyle: 'as',
objectLiteralTypeAssertions: 'allow',
},
],
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/ban-types': 'off', '@typescript-eslint/ban-types': 'off',
'react-hooks/exhaustive-deps': 'off', // Turned off to reduce noise 'react-hooks/exhaustive-deps': 'warn',
'import/named': 'error', 'import/named': 'error',
'import/no-duplicates': 'error', 'import/no-duplicates': 'error',
'import/no-unresolved': 'error', 'import/no-unresolved': 'error',

View File

@ -16,7 +16,7 @@ The frontend is a **Next.js 15** application with **React 19**, **TypeScript**,
|------------|---------|---------| |------------|---------|---------|
| Next.js | 15.3.1 | React framework with Pages Router | | Next.js | 15.3.1 | React framework with Pages Router |
| React | 19.0.0 | UI library | | React | 19.0.0 | UI library |
| TypeScript | 5.x | Type safety | | TypeScript | 5.x | Strict type safety |
| Redux Toolkit | 2.1.0 | State management | | Redux Toolkit | 2.1.0 | State management |
| MUI X DataGrid | 7.0.0 | Data tables | | MUI X DataGrid | 7.0.0 | Data tables |
| Tailwind CSS | 3.4.1 | Utility-first styling | | Tailwind CSS | 3.4.1 | Utility-first styling |

View File

@ -6,6 +6,8 @@ The types module provides **TypeScript type definitions** used throughout the fr
**Location:** `frontend/src/types/` **Location:** `frontend/src/types/`
**Strictness:** Frontend TypeScript runs with `strict: true` in `frontend/tsconfig.json`. `npm run typecheck` is the strict type gate for changed frontend code. ESLint also reports explicit `any`, type assertions/non-null assertions, unused variables/imports, and React hook dependency issues as warnings; the current frontend lint baseline has no warnings.
**Statistics:** **Statistics:**
- **20 files** (~2,205 LOC total) - **20 files** (~2,205 LOC total)
- **6 categories**: Domain Entities, Runtime/Presentation, Infrastructure, Forms/Filters, Specialized, Module Declarations - **6 categories**: Domain Entities, Runtime/Presentation, Infrastructure, Forms/Filters, Specialized, Module Declarations

View File

@ -45,7 +45,10 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.61.1", "@playwright/test": "^1.61.1",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@types/file-saver": "^2.0.7",
"@types/lodash": "^4.17.24",
"@types/node": "18.7.16", "@types/node": "18.7.16",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.62.1", "@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1", "@typescript-eslint/parser": "^8.62.1",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
@ -2573,6 +2576,13 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/file-saver": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/hoist-non-react-statics": { "node_modules/@types/hoist-non-react-statics": {
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
@ -2592,6 +2602,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.7.16", "version": "18.7.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz",
@ -2621,6 +2638,16 @@
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
}, },
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-transition-group": { "node_modules/@types/react-transition-group": {
"version": "4.4.12", "version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",

View File

@ -57,7 +57,10 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.61.1", "@playwright/test": "^1.61.1",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@types/file-saver": "^2.0.7",
"@types/lodash": "^4.17.24",
"@types/node": "18.7.16", "@types/node": "18.7.16",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.62.1", "@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1", "@typescript-eslint/parser": "^8.62.1",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",

View File

@ -105,6 +105,9 @@ export const getButtonColor = (
info: 'border-blue-600 border-blue-600 dark:border-pavitra-blue', info: 'border-blue-600 border-blue-600 dark:border-pavitra-blue',
}, },
text: { text: {
white: 'text-black dark:text-white',
whiteDark: 'text-black dark:text-white',
lightDark: 'text-black dark:text-white',
contrast: 'dark:text-slate-100', contrast: 'dark:text-slate-100',
success: 'text-emerald-600 dark:text-pavitra-blue', success: 'text-emerald-600 dark:text-pavitra-blue',
danger: 'text-red-600 dark:text-red-500', danger: 'text-red-600 dark:text-red-500',
@ -112,6 +115,9 @@ export const getButtonColor = (
info: 'text-blue-600 dark:text-pavitra-blue', info: 'text-blue-600 dark:text-pavitra-blue',
}, },
outlineHover: { outlineHover: {
white: 'hover:bg-gray-100 hover:dark:bg-dark-800',
whiteDark: 'hover:bg-gray-100 hover:dark:bg-dark-800',
lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700',
contrast: contrast:
'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black', 'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black',
success: success:

View File

@ -1,12 +1,9 @@
import React from 'react';
import ImageField from '../ImageField';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import dataFormatter from '../../helpers/dataFormatter';
import { Pagination } from '../Pagination';
import { saveFile } from '../../helpers/fileSaver';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link'; import Link from 'next/link';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
import ListActionsPopover from '../ListActionsPopover';
import LoadingSpinner from '../LoadingSpinner';
import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import type { AccessLog } from '../../types/entities'; import type { AccessLog } from '../../types/entities';
@ -47,7 +44,7 @@ const CardAccess_logs = ({
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8' className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
> >
{!loading && {!loading &&
access_logs.map((item, index) => ( access_logs.map((item) => (
<li <li
key={item.id} key={item.id}
className={`overflow-hidden ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${ className={`overflow-hidden ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${

View File

@ -1,13 +1,10 @@
import React from 'react';
import CardBox from '../CardBox';
import ImageField from '../ImageField';
import dataFormatter from '../../helpers/dataFormatter';
import { saveFile } from '../../helpers/fileSaver';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import { Pagination } from '../Pagination';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link'; import Link from 'next/link';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
import CardBox from '../CardBox';
import ListActionsPopover from '../ListActionsPopover';
import LoadingSpinner from '../LoadingSpinner';
import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import type { AccessLog } from '../../types/entities'; import type { AccessLog } from '../../types/entities';
@ -32,9 +29,6 @@ const ListAccess_logs = ({
const currentUser = useAppSelector((state) => state.auth.currentUser); const currentUser = useAppSelector((state) => state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ACCESS_LOGS'); const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ACCESS_LOGS');
const corners = useAppSelector((state) => state.style.corners);
const bgColor = useAppSelector((state) => state.style.cardsColor);
return ( return (
<> <>
<div className='relative overflow-x-auto p-4 space-y-4'> <div className='relative overflow-x-auto p-4 space-y-4'>

View File

@ -1,13 +1,13 @@
import { createTableComponent } from '../Factory/createTableComponent';
import { import {
fetch,
update,
deleteItem, deleteItem,
setRefetch,
deleteItemsByIds, deleteItemsByIds,
fetch,
setRefetch,
update,
} from '../../stores/access_logs/access_logsSlice'; } from '../../stores/access_logs/access_logsSlice';
import { loadColumns } from './configureAccess_logsCols';
import type { AccessLog } from '../../types/entities'; import type { AccessLog } from '../../types/entities';
import { createTableComponent } from '../Factory/createTableComponent';
import { loadColumns } from './configureAccess_logsCols';
const TableAccess_logs = createTableComponent<AccessLog>({ const TableAccess_logs = createTableComponent<AccessLog>({
entityName: 'access_logs', entityName: 'access_logs',

View File

@ -1,6 +1,6 @@
import { import {
createColumnLoader,
ColumnMetadata, ColumnMetadata,
createColumnLoader,
} from '../DataGrid/configBuilderFactory'; } from '../DataGrid/configBuilderFactory';
const ACCESS_LOGS_COLUMNS: ColumnMetadata[] = [ const ACCESS_LOGS_COLUMNS: ColumnMetadata[] = [

View File

@ -1,4 +1,3 @@
import React from 'react';
import { MenuAsideItem } from '../types/menu'; import { MenuAsideItem } from '../types/menu';
import AsideMenuLayer from './AsideMenuLayer'; import AsideMenuLayer from './AsideMenuLayer';
import OverlayLayer from './OverlayLayer'; import OverlayLayer from './OverlayLayer';

View File

@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react';
import { mdiMinus, mdiPlus } from '@mdi/js'; import { mdiMinus, mdiPlus } from '@mdi/js';
import BaseIcon from './BaseIcon';
import Link from 'next/link'; import Link from 'next/link';
import { getButtonColor } from '../colors';
import AsideMenuList from './AsideMenuList';
import { MenuAsideItem } from '../types/menu';
import { useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { getButtonColor } from '../colors';
import { useAppSelector } from '../stores/hooks';
import { MenuAsideItem } from '../types/menu';
import AsideMenuList from './AsideMenuList';
import BaseIcon from './BaseIcon';
type Props = { type Props = {
item: MenuAsideItem; item: MenuAsideItem;

View File

@ -1,10 +1,9 @@
import { mdiClose } from '@mdi/js';
import React from 'react'; import React from 'react';
import { mdiLogout, mdiClose } from '@mdi/js';
import BaseIcon from './BaseIcon';
import AsideMenuList from './AsideMenuList';
import { MenuAsideItem } from '../types/menu';
import { useAppSelector } from '../stores/hooks'; import { useAppSelector } from '../stores/hooks';
import Link from 'next/link'; import { MenuAsideItem } from '../types/menu';
import AsideMenuList from './AsideMenuList';
import BaseIcon from './BaseIcon';
type Props = { type Props = {
menu: MenuAsideItem[]; menu: MenuAsideItem[];

View File

@ -1,8 +1,7 @@
import React from 'react'; import { hasPermission } from '../helpers/userPermissions';
import { useAppSelector } from '../stores/hooks';
import { MenuAsideItem } from '../types/menu'; import { MenuAsideItem } from '../types/menu';
import AsideMenuItem from './AsideMenuItem'; import AsideMenuItem from './AsideMenuItem';
import { useAppSelector } from '../stores/hooks';
import { hasPermission } from '../helpers/userPermissions';
type Props = { type Props = {
menu: MenuAsideItem[]; menu: MenuAsideItem[];
@ -22,7 +21,9 @@ export default function AsideMenuList({
return ( return (
<ul className={className}> <ul className={className}>
{menu.map((item, index) => { {menu.map((item, index) => {
if (!hasPermission(currentUser, item.permissions)) return null; if (item.permissions && !hasPermission(currentUser, item.permissions)) {
return null;
}
return ( return (
<div key={index}> <div key={index}>

View File

@ -1,12 +1,9 @@
import React from 'react';
import ImageField from '../ImageField';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import dataFormatter from '../../helpers/dataFormatter';
import { Pagination } from '../Pagination';
import { saveFile } from '../../helpers/fileSaver';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link'; import Link from 'next/link';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
import ListActionsPopover from '../ListActionsPopover';
import LoadingSpinner from '../LoadingSpinner';
import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import type { AssetVariant } from '../../types/entities'; import type { AssetVariant } from '../../types/entities';
@ -50,7 +47,7 @@ const CardAsset_variants = ({
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8' className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
> >
{!loading && {!loading &&
asset_variants.map((item, index) => ( asset_variants.map((item) => (
<li <li
key={item.id} key={item.id}
className={`overflow-hidden ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${ className={`overflow-hidden ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${

View File

@ -1,13 +1,10 @@
import React from 'react';
import CardBox from '../CardBox';
import ImageField from '../ImageField';
import dataFormatter from '../../helpers/dataFormatter';
import { saveFile } from '../../helpers/fileSaver';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import { Pagination } from '../Pagination';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link'; import Link from 'next/link';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
import CardBox from '../CardBox';
import ListActionsPopover from '../ListActionsPopover';
import LoadingSpinner from '../LoadingSpinner';
import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import type { AssetVariant } from '../../types/entities'; import type { AssetVariant } from '../../types/entities';
@ -35,9 +32,6 @@ const ListAsset_variants = ({
'UPDATE_ASSET_VARIANTS', 'UPDATE_ASSET_VARIANTS',
); );
const corners = useAppSelector((state) => state.style.corners);
const bgColor = useAppSelector((state) => state.style.cardsColor);
return ( return (
<> <>
<div className='relative overflow-x-auto p-4 space-y-4'> <div className='relative overflow-x-auto p-4 space-y-4'>

View File

@ -1,13 +1,13 @@
import { createTableComponent } from '../Factory/createTableComponent';
import { import {
fetch,
update,
deleteItem, deleteItem,
setRefetch,
deleteItemsByIds, deleteItemsByIds,
fetch,
setRefetch,
update,
} from '../../stores/asset_variants/asset_variantsSlice'; } from '../../stores/asset_variants/asset_variantsSlice';
import { loadColumns } from './configureAsset_variantsCols';
import type { AssetVariant } from '../../types/entities'; import type { AssetVariant } from '../../types/entities';
import { createTableComponent } from '../Factory/createTableComponent';
import { loadColumns } from './configureAsset_variantsCols';
const TableAsset_variants = createTableComponent<AssetVariant>({ const TableAsset_variants = createTableComponent<AssetVariant>({
entityName: 'asset_variants', entityName: 'asset_variants',

View File

@ -1,6 +1,6 @@
import { import {
createColumnLoader,
ColumnMetadata, ColumnMetadata,
createColumnLoader,
} from '../DataGrid/configBuilderFactory'; } from '../DataGrid/configBuilderFactory';
const ASSET_VARIANTS_COLUMNS: ColumnMetadata[] = [ const ASSET_VARIANTS_COLUMNS: ColumnMetadata[] = [

View File

@ -92,7 +92,7 @@ const AssetSectionCard: React.FC<AssetSectionCardProps> = ({
<input <input
type='file' type='file'
multiple multiple
accept={section.accept} accept={section.accept ?? undefined}
className='w-full border border-gray-300 rounded px-2 py-2 mb-3 bg-white dark:bg-dark-800' className='w-full border border-gray-300 rounded px-2 py-2 mb-3 bg-white dark:bg-dark-800'
disabled={isUploading || disabled || !hasCreatePermission} disabled={isUploading || disabled || !hasCreatePermission}
onChange={handleFileChange} onChange={handleFileChange}

View File

@ -1,12 +1,9 @@
import React from 'react';
import ImageField from '../ImageField';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import dataFormatter from '../../helpers/dataFormatter';
import { Pagination } from '../Pagination';
import { saveFile } from '../../helpers/fileSaver';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link'; import Link from 'next/link';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
import ListActionsPopover from '../ListActionsPopover';
import LoadingSpinner from '../LoadingSpinner';
import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import type { Asset } from '../../types/entities'; import type { Asset } from '../../types/entities';
@ -47,7 +44,7 @@ const CardAssets = ({
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8' className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
> >
{!loading && {!loading &&
assets.map((item, index) => ( assets.map((item) => (
<li <li
key={item.id} key={item.id}
className={`overflow-hidden ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${ className={`overflow-hidden ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${

View File

@ -5,13 +5,13 @@
* Validates embed URLs against a trusted domain allowlist on the backend. * Validates embed URLs against a trusted domain allowlist on the backend.
*/ */
import React, { useState, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import React, { useCallback, useState } from 'react';
import { create, deleteItem } from '../../stores/assets/assetsSlice';
import { queryKeys } from '../../lib/queryClient'; import { queryKeys } from '../../lib/queryClient';
import CardBox from '../CardBox'; import { create, deleteItem } from '../../stores/assets/assetsSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import BaseButton from '../BaseButton'; import BaseButton from '../BaseButton';
import CardBox from '../CardBox';
import type { Asset } from './AssetSectionCard'; import type { Asset } from './AssetSectionCard';
interface EmbedAssetSectionProps { interface EmbedAssetSectionProps {

View File

@ -1,13 +1,10 @@
import React from 'react';
import CardBox from '../CardBox';
import ImageField from '../ImageField';
import dataFormatter from '../../helpers/dataFormatter';
import { saveFile } from '../../helpers/fileSaver';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import { Pagination } from '../Pagination';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link'; import Link from 'next/link';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
import CardBox from '../CardBox';
import ListActionsPopover from '../ListActionsPopover';
import LoadingSpinner from '../LoadingSpinner';
import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import type { Asset } from '../../types/entities'; import type { Asset } from '../../types/entities';
@ -32,9 +29,6 @@ const ListAssets = ({
const currentUser = useAppSelector((state) => state.auth.currentUser); const currentUser = useAppSelector((state) => state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ASSETS'); const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ASSETS');
const corners = useAppSelector((state) => state.style.corners);
const bgColor = useAppSelector((state) => state.style.cardsColor);
return ( return (
<> <>
<div className='relative overflow-x-auto p-4 space-y-4'> <div className='relative overflow-x-auto p-4 space-y-4'>

View File

@ -1,13 +1,13 @@
import { createTableComponent } from '../Factory/createTableComponent';
import { import {
fetch,
update,
deleteItem, deleteItem,
setRefetch,
deleteItemsByIds, deleteItemsByIds,
fetch,
setRefetch,
update,
} from '../../stores/assets/assetsSlice'; } from '../../stores/assets/assetsSlice';
import { loadColumns } from './configureAssetsCols';
import type { Asset } from '../../types/entities'; import type { Asset } from '../../types/entities';
import { createTableComponent } from '../Factory/createTableComponent';
import { loadColumns } from './configureAssetsCols';
const TableAssets = createTableComponent<Asset>({ const TableAssets = createTableComponent<Asset>({
entityName: 'assets', entityName: 'assets',

View File

@ -1,6 +1,6 @@
import { import {
createColumnLoader,
ColumnMetadata, ColumnMetadata,
createColumnLoader,
} from '../DataGrid/configBuilderFactory'; } from '../DataGrid/configBuilderFactory';
const ASSETS_COLUMNS: ColumnMetadata[] = [ const ASSETS_COLUMNS: ColumnMetadata[] = [

View File

@ -1,15 +1,15 @@
import axios from 'axios'; import axios from 'axios';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { logger } from '../../lib/logger';
import {
isAudioMimeType,
isVideoMimeType,
probeMediaDuration,
} from '../../lib/mediaDuration';
import FileUploader from '../Uploaders/UploadService'; import FileUploader from '../Uploaders/UploadService';
import type { AssetSection } from './AssetSectionCard'; import type { AssetSection } from './AssetSectionCard';
import type { UploadQueueItem } from './UploadProgressList'; import type { UploadQueueItem } from './UploadProgressList';
import { logger } from '../../lib/logger';
import {
probeMediaDuration,
isVideoMimeType,
isAudioMimeType,
} from '../../lib/mediaDuration';
interface UseAssetUploaderOptions { interface UseAssetUploaderOptions {
selectedProjectId: string; selectedProjectId: string;

View File

@ -11,11 +11,11 @@
import React, { import React, {
createContext, createContext,
useCallback,
useContext, useContext,
useMemo,
useRef, useRef,
useState, useState,
useCallback,
useMemo,
} from 'react'; } from 'react';
interface BackdropItem { interface BackdropItem {

View File

@ -1,9 +1,9 @@
import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react';
import { getButtonColor } from '../colors'; import { getButtonColor } from '../colors';
import BaseIcon from './BaseIcon';
import type { ColorButtonKey } from '../types/ui';
import { useAppSelector } from '../stores/hooks'; import { useAppSelector } from '../stores/hooks';
import type { ColorButtonKey } from '../types/ui';
import BaseIcon from './BaseIcon';
type Props = { type Props = {
label?: string; label?: string;

View File

@ -1,5 +1,5 @@
import { Children, cloneElement, ReactElement } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Children, cloneElement, isValidElement } from 'react';
type Props = { type Props = {
type?: string; type?: string;
@ -24,13 +24,15 @@ const BaseButtons = ({
noWrap ? 'flex-nowrap' : 'flex-wrap' noWrap ? 'flex-nowrap' : 'flex-wrap'
}`} }`}
> >
{Children.map(children, (child: ReactElement) => {Children.map(children, (child) => {
child if (!isValidElement<{ className?: string }>(child)) {
? cloneElement(child as ReactElement<{ className?: string }>, { return child;
className: `${classAddon} ${(child.props as { className?: string }).className || ''}`, }
})
: null, return cloneElement(child, {
)} className: `${classAddon} ${child.props.className || ''}`,
});
})}
</div> </div>
); );
}; };

View File

@ -1,4 +1,3 @@
import React from 'react';
import { useAppSelector } from '../stores/hooks'; import { useAppSelector } from '../stores/hooks';
type Props = { type Props = {
navBar?: boolean; navBar?: boolean;

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import { ReactNode } from 'react';
type Props = { type Props = {
path: string; path: string;

View File

@ -6,7 +6,7 @@
* Includes delay to avoid flashing for quick operations. * Includes delay to avoid flashing for quick operations.
*/ */
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { PRELOAD_CONFIG } from '../config/preload.config'; import { PRELOAD_CONFIG } from '../config/preload.config';
interface CanvasLoadingSpinnerProps { interface CanvasLoadingSpinnerProps {

View File

@ -1,7 +1,7 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentBody from './CardBoxComponentBody'; import CardBoxComponentBody from './CardBoxComponentBody';
import CardBoxComponentFooter from './CardBoxComponentFooter'; import CardBoxComponentFooter from './CardBoxComponentFooter';
import { useAppSelector } from '../stores/hooks';
type Props = { type Props = {
rounded?: string; rounded?: string;
@ -20,7 +20,6 @@ type Props = {
}; };
export default function CardBox({ export default function CardBox({
rounded = 'rounded',
flex = 'flex-col', flex = 'flex-col',
className = '', className = '',
hasComponentLayout = false, hasComponentLayout = false,
@ -28,7 +27,6 @@ export default function CardBox({
hasTable = false, hasTable = false,
isHoverable = false, isHoverable = false,
isList = false, isList = false,
isModal = false,
children, children,
footer, footer,
id = '', id = '',

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import { ReactNode } from 'react';
type Props = { type Props = {
noPadding?: boolean; noPadding?: boolean;

View File

@ -1,5 +1,3 @@
import React from 'react';
const CardBoxComponentEmpty = () => { const CardBoxComponentEmpty = () => {
return ( return (
<div className='text-center py-24 text-gray-500 dark:text-slate-400'> <div className='text-center py-24 text-gray-500 dark:text-slate-400'>

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import { ReactNode } from 'react';
type Props = { type Props = {
className?: string; className?: string;

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import { ReactNode } from 'react';
type Props = { type Props = {
title: string; title: string;

View File

@ -1,5 +1,5 @@
import { mdiClose } from '@mdi/js'; import { mdiClose } from '@mdi/js';
import { ReactNode } from 'react'; import type { MouseEvent, ReactNode } from 'react';
import type { ColorButtonKey } from '../types/ui'; import type { ColorButtonKey } from '../types/ui';
import BaseButton from './BaseButton'; import BaseButton from './BaseButton';
import BaseButtons from './BaseButtons'; import BaseButtons from './BaseButtons';
@ -32,12 +32,20 @@ const CardBoxModal = ({
return null; return null;
} }
const handleConfirm = (_event: MouseEvent) => {
onConfirm();
};
const handleCancel = (_event: MouseEvent) => {
onCancel?.();
};
const footer = ( const footer = (
<BaseButtons> <BaseButtons>
<BaseButton <BaseButton
label={buttonLabel} label={buttonLabel}
color={buttonColor} color={buttonColor}
onClick={onConfirm} onClick={handleConfirm}
disabled={isConfirmDisabled} disabled={isConfirmDisabled}
/> />
{!!onCancel && ( {!!onCancel && (
@ -45,7 +53,7 @@ const CardBoxModal = ({
label='Cancel' label='Cancel'
color={buttonColor} color={buttonColor}
outline outline
onClick={onCancel} onClick={handleCancel}
/> />
)} )}
</BaseButtons> </BaseButtons>
@ -53,7 +61,7 @@ const CardBoxModal = ({
return ( return (
<OverlayLayer <OverlayLayer
onClick={onCancel} onClick={handleCancel}
className={onCancel ? 'cursor-pointer' : ''} className={onCancel ? 'cursor-pointer' : ''}
> >
<CardBox <CardBox
@ -66,7 +74,7 @@ const CardBoxModal = ({
<BaseButton <BaseButton
icon={mdiClose} icon={mdiClose}
color='whiteDark' color='whiteDark'
onClick={onCancel} onClick={handleCancel}
small small
roundedFull roundedFull
/> />

View File

@ -1,15 +1,15 @@
import React, { import {
MutableRefObject,
ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
ReactNode,
MutableRefObject,
} from 'react'; } from 'react';
interface ClickOutsideProps { interface ClickOutsideProps {
children?: ReactNode; children?: ReactNode;
onClickOutside: () => void; onClickOutside: () => void;
excludedElements: MutableRefObject<any>[]; excludedElements: MutableRefObject<HTMLElement | null>[];
} }
const ClickOutside = ({ const ClickOutside = ({
@ -17,19 +17,21 @@ const ClickOutside = ({
onClickOutside, onClickOutside,
excludedElements, excludedElements,
}: ClickOutsideProps) => { }: ClickOutsideProps) => {
const wrapperRef = useRef(null); const wrapperRef = useRef<HTMLDivElement | null>(null);
const handleClickOutside = useCallback( const handleClickOutside = useCallback(
(event) => { (event: MouseEvent) => {
const target = event.target;
if (!(target instanceof Node)) return;
if ( if (
wrapperRef.current && wrapperRef.current &&
!wrapperRef.current.contains(event.target) && !wrapperRef.current.contains(target) &&
!excludedElements.some((el) => el.current.contains(event.target)) !excludedElements.some((el) => el.current?.contains(target))
) { ) {
onClickOutside(); onClickOutside();
} }
}, },
[wrapperRef, onClickOutside, ...excludedElements], [excludedElements, onClickOutside],
); );
useEffect(() => { useEffect(() => {

View File

@ -6,8 +6,8 @@
*/ */
import React from 'react'; import React from 'react';
import type { AssetOption } from './types';
import { addFallbackAssetOption } from '../../lib/constructorHelpers'; import { addFallbackAssetOption } from '../../lib/constructorHelpers';
import type { AssetOption } from './types';
interface AssetSelectCompactProps { interface AssetSelectCompactProps {
label: string; label: string;

View File

@ -7,12 +7,12 @@
*/ */
import React from 'react'; import React from 'react';
import { addFallbackAssetOption } from '../../lib/constructorHelpers';
import type { import type {
AssetOption, AssetOption,
VideoPlaybackSettings,
AudioPlaybackSettings, AudioPlaybackSettings,
VideoPlaybackSettings,
} from './types'; } from './types';
import { addFallbackAssetOption } from '../../lib/constructorHelpers';
interface BackgroundSettingsEditorProps { interface BackgroundSettingsEditorProps {
type: 'image' | 'video' | 'embed' | 'audio'; type: 'image' | 'video' | 'embed' | 'audio';

View File

@ -6,21 +6,20 @@
* Supports custom video playback settings (autoplay, loop, muted, start/end time). * Supports custom video playback settings (autoplay, loop, muted, start/end time).
*/ */
import React, { useEffect, useState, useMemo, useCallback } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useBackgroundVideoPlayback } from '../../hooks/useBackgroundVideoPlayback';
import { useBackgroundAudioPlayback } from '../../hooks/useBackgroundAudioPlayback';
import PreviousBackgroundOverlay from '../PreviousBackgroundOverlay';
import { baseURLApi } from '../../config'; import { baseURLApi } from '../../config';
import { useBackgroundAudioPlayback } from '../../hooks/useBackgroundAudioPlayback';
import { useBackgroundVideoPlayback } from '../../hooks/useBackgroundVideoPlayback';
import { buildChromeFreeEmbedUrl } from '../../lib/embedUrl'; import { buildChromeFreeEmbedUrl } from '../../lib/embedUrl';
import CanvasBackgroundAudioLayer from './CanvasBackgroundAudioLayer'; import PreviousBackgroundOverlay from '../PreviousBackgroundOverlay';
import CanvasBackgroundEmbedLayer from './CanvasBackgroundEmbedLayer';
import { import {
getActiveCanvasVideoUrl, getActiveCanvasVideoUrl,
getCanvasVideoSrc, getCanvasVideoSrc,
isBlobUrl,
scheduleAfterPaint, scheduleAfterPaint,
shouldUseNativeVideoLoop, shouldUseNativeVideoLoop,
} from './CanvasBackground.helpers'; } from './CanvasBackground.helpers';
import CanvasBackgroundAudioLayer from './CanvasBackgroundAudioLayer';
import CanvasBackgroundEmbedLayer from './CanvasBackgroundEmbedLayer';
import CanvasBackgroundImageLayer from './CanvasBackgroundImageLayer'; import CanvasBackgroundImageLayer from './CanvasBackgroundImageLayer';
import CanvasBackgroundVideoLayer from './CanvasBackgroundVideoLayer'; import CanvasBackgroundVideoLayer from './CanvasBackgroundVideoLayer';
import { useCanvasBackgroundImageReady } from './useCanvasBackgroundImageReady'; import { useCanvasBackgroundImageReady } from './useCanvasBackgroundImageReady';

View File

@ -1,5 +1,5 @@
import React from 'react';
import NextImage from 'next/image'; import NextImage from 'next/image';
import React from 'react';
import { isBlobUrl } from './CanvasBackground.helpers'; import { isBlobUrl } from './CanvasBackground.helpers';
interface CanvasBackgroundImageLayerProps { interface CanvasBackgroundImageLayerProps {

View File

@ -8,20 +8,20 @@
*/ */
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import UiElementRenderer from '../UiElements/UiElementRenderer';
import { useElementEffects } from '../../hooks/useElementEffects';
import { useAudioEffects } from '../../hooks/useAudioEffects'; import { useAudioEffects } from '../../hooks/useAudioEffects';
import { useElementEffects } from '../../hooks/useElementEffects';
import type { PreloadCacheProvider } from '../../hooks/video';
import { import {
buildTransitionStyle,
buildAppearAnimationStyle, buildAppearAnimationStyle,
buildTransitionStyle,
hasAnyEffects, hasAnyEffects,
type ElementEffectProperties, type ElementEffectProperties,
} from '../../lib/elementEffects'; } from '../../lib/elementEffects';
import { normalizeZIndexValue } from '../../lib/elementStyles';
import { isInfoPanelElementType } from '../../lib/elementTypeGuards';
import type { CanvasElement as CanvasElementType } from '../../types/constructor'; import type { CanvasElement as CanvasElementType } from '../../types/constructor';
import type { ResolvedTransitionSettings } from '../../types/transition'; import type { ResolvedTransitionSettings } from '../../types/transition';
import type { PreloadCacheProvider } from '../../hooks/video'; import UiElementRenderer from '../UiElements/UiElementRenderer';
import { isInfoPanelElementType } from '../../lib/elementTypeGuards';
import { normalizeZIndexValue } from '../../lib/elementStyles';
interface CanvasElementProps { interface CanvasElementProps {
element: CanvasElementType; element: CanvasElementType;
@ -180,6 +180,7 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
// Check if we need the inner wrapper for effects // Check if we need the inner wrapper for effects
const needsEffectWrapper = !isEditMode && hasAnyEffects(effectProperties); const needsEffectWrapper = !isEditMode && hasAnyEffects(effectProperties);
const { onMouseDown: eventMouseDown, ...outerEventHandlers } = eventHandlers;
return ( return (
<div <div
@ -188,11 +189,17 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
data-constructor-element-id={element.id} data-constructor-element-id={element.id}
className='absolute cursor-pointer' className='absolute cursor-pointer'
style={positionStyle} style={positionStyle}
onMouseDown={isEditMode ? onMouseDown : undefined} onMouseDown={
isEditMode
? onMouseDown
: !needsEffectWrapper
? eventMouseDown
: undefined
}
onClick={handleClick} onClick={handleClick}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
aria-disabled={isDisabled} aria-disabled={isDisabled}
{...(!isEditMode && !needsEffectWrapper ? eventHandlers : {})} {...(!isEditMode && !needsEffectWrapper ? outerEventHandlers : {})}
> >
{needsEffectWrapper ? ( {needsEffectWrapper ? (
// Inner wrapper handles hover/focus/active effects independently from animation // Inner wrapper handles hover/focus/active effects independently from animation

View File

@ -1,15 +1,15 @@
import { mdiPlus } from '@mdi/js'; import { mdiPlus } from '@mdi/js';
import type { CSSProperties, MouseEvent } from 'react'; import type { CSSProperties, MouseEvent } from 'react';
import BaseButton from '../BaseButton'; import type { PreloadCacheProvider } from '../../hooks/video';
import CanvasElementComponent from './CanvasElement'; import { isElementFlagEnabled } from '../../lib/elementFlags';
import {
isInfoPanelElementType,
isNavigationElementType,
} from '../../lib/elementTypeGuards';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import type { ResolvedTransitionSettings } from '../../types/transition'; import type { ResolvedTransitionSettings } from '../../types/transition';
import type { PreloadCacheProvider } from '../../hooks/video'; import BaseButton from '../BaseButton';
import { import CanvasElementComponent from './CanvasElement';
isNavigationElementType,
isInfoPanelElementType,
} from '../../lib/elementTypeGuards';
import { isElementFlagEnabled } from '../../lib/elementFlags';
import { shouldRenderConstructorCanvasElement } from './constructorPage.helpers'; import { shouldRenderConstructorCanvasElement } from './constructorPage.helpers';
type ConstructorCanvasElementsLayerProps = { type ConstructorCanvasElementsLayerProps = {
@ -22,7 +22,7 @@ type ConstructorCanvasElementsLayerProps = {
letterboxStyles?: CSSProperties; letterboxStyles?: CSSProperties;
pageTransitionSettings: ResolvedTransitionSettings; pageTransitionSettings: ResolvedTransitionSettings;
preloadCache: PreloadCacheProvider; preloadCache: PreloadCacheProvider;
resolveUrl: (url: string) => string; resolveUrl: (url: string | undefined) => string;
isElementVisible: (element: CanvasElement) => boolean; isElementVisible: (element: CanvasElement) => boolean;
isInfoPanelOpen: (elementId: string) => boolean; isInfoPanelOpen: (elementId: string) => boolean;
onCreateFirstPage: () => void; onCreateFirstPage: () => void;
@ -31,7 +31,7 @@ type ConstructorCanvasElementsLayerProps = {
onGalleryCardClick: (element: CanvasElement, cardIndex: number) => void; onGalleryCardClick: (element: CanvasElement, cardIndex: number) => void;
onCarouselButtonPositionChange: ( onCarouselButtonPositionChange: (
elementId: string, elementId: string,
button: 'prev' | 'next' | 'back', button: 'prev' | 'next',
x: number, x: number,
y: number, y: number,
) => void; ) => void;

View File

@ -1,11 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { TransitionPreviewState } from '../../types/presentation';
import { import {
shouldShowConstructorCanvasElements, shouldShowConstructorCanvasElements,
shouldShowConstructorCanvasSpinner, shouldShowConstructorCanvasSpinner,
} from './ConstructorCanvasStage.helpers'; } from './ConstructorCanvasStage.helpers';
import type { TransitionPreviewState } from '../../types/presentation';
const transitionPreview = { const transitionPreview = {
videoUrl: 'assets/transition.mp4', videoUrl: 'assets/transition.mp4',

View File

@ -1,14 +1,7 @@
import type { CSSProperties, MouseEvent, PointerEvent, RefObject } from 'react'; import type { CSSProperties, MouseEvent, PointerEvent, RefObject } from 'react';
import { BackdropPortalProvider } from '../BackdropPortal';
import CanvasLoadingSpinner from '../CanvasLoadingSpinner';
import RuntimeControls from '../Runtime/RuntimeControls';
import CanvasBackground from './CanvasBackground';
import ConstructorCanvasElementsLayer from './ConstructorCanvasElementsLayer';
import ElementEditorPanel from './ElementEditorPanel';
import TransitionBlackOverlay from '../TransitionBlackOverlay';
import { isSafari } from '../../lib/browserUtils';
import type { PreloadCacheProvider } from '../../hooks/video'; import type { PreloadCacheProvider } from '../../hooks/video';
import { isSafari } from '../../lib/browserUtils';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities'; import type { TourPage } from '../../types/entities';
import type { TransitionPreviewState } from '../../types/presentation'; import type { TransitionPreviewState } from '../../types/presentation';
@ -17,10 +10,17 @@ import type {
ResolvedUiControlsSettings, ResolvedUiControlsSettings,
SystemUiControlType, SystemUiControlType,
} from '../../types/uiControls'; } from '../../types/uiControls';
import { BackdropPortalProvider } from '../BackdropPortal';
import CanvasLoadingSpinner from '../CanvasLoadingSpinner';
import RuntimeControls from '../Runtime/RuntimeControls';
import TransitionBlackOverlay from '../TransitionBlackOverlay';
import CanvasBackground from './CanvasBackground';
import ConstructorCanvasElementsLayer from './ConstructorCanvasElementsLayer';
import { import {
shouldShowConstructorCanvasElements, shouldShowConstructorCanvasElements,
shouldShowConstructorCanvasSpinner, shouldShowConstructorCanvasSpinner,
} from './ConstructorCanvasStage.helpers'; } from './ConstructorCanvasStage.helpers';
import ElementEditorPanel from './ElementEditorPanel';
type Position = { type Position = {
x: number; x: number;
@ -71,11 +71,11 @@ type ConstructorCanvasStageProps = {
videoAutoplay: boolean; videoAutoplay: boolean;
videoLoop: boolean; videoLoop: boolean;
videoMuted: boolean; videoMuted: boolean;
videoStartTime?: number; videoStartTime?: number | null;
videoEndTime?: number; videoEndTime?: number | null;
audioLoop: boolean; audioLoop: boolean;
audioStartTime?: number; audioStartTime?: number | null;
audioEndTime?: number; audioEndTime?: number | null;
}; };
soundControl: { soundControl: {
isMuted: boolean; isMuted: boolean;
@ -110,7 +110,7 @@ type ConstructorCanvasStageProps = {
onGalleryCardClick: (element: CanvasElement, cardIndex: number) => void; onGalleryCardClick: (element: CanvasElement, cardIndex: number) => void;
onCarouselButtonPositionChange: ( onCarouselButtonPositionChange: (
elementId: string, elementId: string,
button: 'prev' | 'next' | 'back', button: 'prev' | 'next',
x: number, x: number,
y: number, y: number,
) => void; ) => void;

View File

@ -1,6 +1,6 @@
import CardBoxModal from '../CardBoxModal'; import CardBoxModal from '../CardBoxModal';
import CreatePageModal from './CreatePageModal';
import { getConstructorDeletePageName } from './ConstructorPageModals.helpers'; import { getConstructorDeletePageName } from './ConstructorPageModals.helpers';
import CreatePageModal from './CreatePageModal';
type ConstructorPageModalsProps = { type ConstructorPageModalsProps = {
isCreatePageModalActive: boolean; isCreatePageModalActive: boolean;

View File

@ -1,11 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { InfoPanelImage } from '../../types/infoPanel';
import { import {
getConstructorInfoPanelDetailImage, getConstructorInfoPanelDetailImage,
shouldRenderConstructorImageDetailPanel, shouldRenderConstructorImageDetailPanel,
} from './ConstructorRuntimeOverlays.helpers'; } from './ConstructorRuntimeOverlays.helpers';
import type { InfoPanelImage } from '../../types/infoPanel';
const image = { id: 'image-1', url: 'assets/image.jpg' } as InfoPanelImage; const image = { id: 'image-1', url: 'assets/image.jpg' } as InfoPanelImage;

View File

@ -4,7 +4,7 @@ export const getConstructorInfoPanelDetailImage = ({
activeDetailImages, activeDetailImages,
panelId, panelId,
}: { }: {
activeDetailImages: Record<string, InfoPanelImage | undefined>; activeDetailImages: Record<string, InfoPanelImage | null | undefined>;
panelId: string; panelId: string;
}) => activeDetailImages[panelId]; }) => activeDetailImages[panelId];
@ -12,6 +12,6 @@ export const shouldRenderConstructorImageDetailPanel = ({
image, image,
isEditMode, isEditMode,
}: { }: {
image?: InfoPanelImage; image?: InfoPanelImage | null;
isEditMode: boolean; isEditMode: boolean;
}) => Boolean(image) || isEditMode; }) => Boolean(image) || isEditMode;

View File

@ -1,14 +1,14 @@
import { Fragment, type CSSProperties } from 'react'; import { Fragment, type CSSProperties } from 'react';
import GalleryCarouselOverlay from '../UiElements/GalleryCarouselOverlay';
import ImageDetailPanel from '../UiElements/ImageDetailPanel';
import InfoPanelOverlay from '../UiElements/InfoPanelOverlay';
import type { import type {
CanvasElement, CanvasElement,
GalleryCarouselMediaItem, GalleryCarouselMediaItem,
} from '../../types/constructor'; } from '../../types/constructor';
import type { InfoPanelImage } from '../../types/infoPanel'; import type { InfoPanelImage } from '../../types/infoPanel';
import type { ResolvedTransitionSettings } from '../../types/transition'; import type { ResolvedTransitionSettings } from '../../types/transition';
import GalleryCarouselOverlay from '../UiElements/GalleryCarouselOverlay';
import ImageDetailPanel from '../UiElements/ImageDetailPanel';
import InfoPanelOverlay from '../UiElements/InfoPanelOverlay';
import { import {
getConstructorInfoPanelDetailImage, getConstructorInfoPanelDetailImage,
shouldRenderConstructorImageDetailPanel, shouldRenderConstructorImageDetailPanel,
@ -32,7 +32,7 @@ type ConstructorRuntimeOverlaysProps = {
activeInfoPanelGalleryElement: CanvasElement | null; activeInfoPanelGalleryElement: CanvasElement | null;
shouldShowInfoPanelOverlays: boolean; shouldShowInfoPanelOverlays: boolean;
infoPanelElementsToRender: CanvasElement[]; infoPanelElementsToRender: CanvasElement[];
activeDetailImages: Record<string, InfoPanelImage | undefined>; activeDetailImages: Record<string, InfoPanelImage | null | undefined>;
isEditMode: boolean; isEditMode: boolean;
letterboxStyles: CSSProperties; letterboxStyles: CSSProperties;
cssVars: CSSProperties; cssVars: CSSProperties;
@ -42,7 +42,7 @@ type ConstructorRuntimeOverlaysProps = {
onCloseInfoPanelGallery: () => void; onCloseInfoPanelGallery: () => void;
onCloseAllInfoPanels: () => void; onCloseAllInfoPanels: () => void;
onCloseInfoPanel: (panelId: string) => void; onCloseInfoPanel: (panelId: string) => void;
onSetDetailImage: (panelId: string, image: InfoPanelImage) => void; onSetDetailImage: (panelId: string, image: InfoPanelImage | null) => void;
onCloseDetailImage: (panelId: string) => void; onCloseDetailImage: (panelId: string) => void;
onOpenInfoPanelGallery: ( onOpenInfoPanelGallery: (
panelId: string, panelId: string,
@ -220,7 +220,7 @@ const ConstructorRuntimeOverlays = ({
}) && ( }) && (
<ImageDetailPanel <ImageDetailPanel
element={infoPanelElementToRender} element={infoPanelElementToRender}
image={panelDetailImage} image={panelDetailImage ?? null}
onClose={() => onCloseDetailImage(infoPanelElementToRender.id)} onClose={() => onCloseDetailImage(infoPanelElementToRender.id)}
resolveUrl={resolveUrl} resolveUrl={resolveUrl}
letterboxStyles={letterboxStyles} letterboxStyles={letterboxStyles}

View File

@ -1,12 +1,12 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { TourPage } from './types';
import { import {
getCollapsedToolbarPageName, getCollapsedToolbarPageName,
getConstructorToolbarMaxWidth,
getConstructorToolbarActionState, getConstructorToolbarActionState,
getConstructorToolbarMaxWidth,
sortToolbarPages, sortToolbarPages,
} from './ConstructorToolbar.helpers'; } from './ConstructorToolbar.helpers';
import type { TourPage } from './types';
const makePage = (id: string, name: string, sortOrder?: number): TourPage => const makePage = (id: string, name: string, sortOrder?: number): TourPage =>
({ ({

View File

@ -5,19 +5,19 @@
* Glassmorphism styling with draggable positioning. * Glassmorphism styling with draggable positioning.
*/ */
import { useState, useRef, useEffect, forwardRef } from 'react';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import { forwardRef, useEffect, useRef, useState } from 'react';
import BaseIcon from '../BaseIcon'; import BaseIcon from '../BaseIcon';
import InteractionModeToggle from './InteractionModeToggle';
import ConstructorToolbarCollapsed from './ConstructorToolbarCollapsed';
import ConstructorToolbarElementActions from './ConstructorToolbarElementActions';
import ConstructorToolbarPageActions from './ConstructorToolbarPageActions';
import ConstructorToolbarSaveControls from './ConstructorToolbarSaveControls';
import { import {
getConstructorToolbarActionState, getConstructorToolbarActionState,
getConstructorToolbarMaxWidth, getConstructorToolbarMaxWidth,
type ToolbarDropdown, type ToolbarDropdown,
} from './ConstructorToolbar.helpers'; } from './ConstructorToolbar.helpers';
import ConstructorToolbarCollapsed from './ConstructorToolbarCollapsed';
import ConstructorToolbarElementActions from './ConstructorToolbarElementActions';
import ConstructorToolbarPageActions from './ConstructorToolbarPageActions';
import ConstructorToolbarSaveControls from './ConstructorToolbarSaveControls';
import InteractionModeToggle from './InteractionModeToggle';
import type { ConstructorToolbarProps } from './types'; import type { ConstructorToolbarProps } from './types';
const ConstructorToolbar = forwardRef<HTMLDivElement, ConstructorToolbarProps>( const ConstructorToolbar = forwardRef<HTMLDivElement, ConstructorToolbarProps>(

View File

@ -1,8 +1,8 @@
import { mdiChevronRight, mdiDotsVertical } from '@mdi/js';
import type { ForwardedRef, MouseEvent } from 'react'; import type { ForwardedRef, MouseEvent } from 'react';
import BaseIcon from '../BaseIcon'; import BaseIcon from '../BaseIcon';
import { mdiChevronRight, mdiDotsVertical } from '@mdi/js';
import type { Position, TourPage } from './types';
import { getCollapsedToolbarPageName } from './ConstructorToolbar.helpers'; import { getCollapsedToolbarPageName } from './ConstructorToolbar.helpers';
import type { Position, TourPage } from './types';
interface Props { interface Props {
position: Position; position: Position;

View File

@ -1,4 +1,3 @@
import type { RefObject } from 'react';
import { import {
mdiChevronDown, mdiChevronDown,
mdiContentDuplicate, mdiContentDuplicate,
@ -12,10 +11,11 @@ import {
mdiVideo, mdiVideo,
mdiViewCarousel, mdiViewCarousel,
} from '@mdi/js'; } from '@mdi/js';
import type { RefObject } from 'react';
import type { CanvasElementType } from '../../types/constructor';
import BaseIcon from '../BaseIcon'; import BaseIcon from '../BaseIcon';
import ClickOutside from '../ClickOutside'; import ClickOutside from '../ClickOutside';
import MenuActionButton from './MenuActionButton'; import MenuActionButton from './MenuActionButton';
import type { CanvasElementType } from '../../types/constructor';
import type { NavigationElementType } from './types'; import type { NavigationElementType } from './types';
interface Props { interface Props {

View File

@ -1,12 +1,12 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { TourPage } from '../../types/entities';
import { import {
findConstructorToolbarPage, findConstructorToolbarPage,
getConstructorExitHref, getConstructorExitHref,
shouldShowConstructorToolbar, shouldShowConstructorToolbar,
} from './ConstructorToolbarLayer.helpers'; } from './ConstructorToolbarLayer.helpers';
import type { TourPage } from '../../types/entities';
test('shouldShowConstructorToolbar requires pages and normal constructor mode', () => { test('shouldShowConstructorToolbar requires pages and normal constructor mode', () => {
assert.equal( assert.equal(

View File

@ -1,17 +1,17 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import type { NavigationElementType } from '../../context/ConstructorContext';
import type {
CanvasElementType,
EditorMenuItem,
} from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import ConstructorToolbar from './ConstructorToolbar'; import ConstructorToolbar from './ConstructorToolbar';
import { import {
findConstructorToolbarPage, findConstructorToolbarPage,
getConstructorExitHref, getConstructorExitHref,
shouldShowConstructorToolbar, shouldShowConstructorToolbar,
} from './ConstructorToolbarLayer.helpers'; } from './ConstructorToolbarLayer.helpers';
import type {
CanvasElementType,
EditorMenuItem,
} from '../../types/constructor';
import type { NavigationElementType } from '../../context/ConstructorContext';
import type { TourPage } from '../../types/entities';
import type { ConstructorInteractionMode, Position } from './types'; import type { ConstructorInteractionMode, Position } from './types';
type ConstructorToolbarLayerProps = { type ConstructorToolbarLayerProps = {
@ -19,7 +19,7 @@ type ConstructorToolbarLayerProps = {
position: Position; position: Position;
isElementEditMode: boolean; isElementEditMode: boolean;
pages: TourPage[]; pages: TourPage[];
activePageId: string; activePageId: string | null;
projectId: string; projectId: string;
isReorderingPages: boolean; isReorderingPages: boolean;
isSaving: boolean; isSaving: boolean;
@ -100,7 +100,7 @@ const ConstructorToolbarLayer = ({
position={position} position={position}
onDragStart={onDragStart} onDragStart={onDragStart}
pages={pages} pages={pages}
activePageId={activePageId} activePageId={activePageId || ''}
onPageChange={(pageId) => { onPageChange={(pageId) => {
const page = findConstructorToolbarPage(pages, pageId); const page = findConstructorToolbarPage(pages, pageId);
if (page) onSwitchToPage(page); if (page) onSwitchToPage(page);

View File

@ -1,4 +1,3 @@
import type { RefObject } from 'react';
import { import {
mdiChevronDown, mdiChevronDown,
mdiChevronUp, mdiChevronUp,
@ -10,11 +9,12 @@ import {
mdiPlus, mdiPlus,
mdiVideo, mdiVideo,
} from '@mdi/js'; } from '@mdi/js';
import type { RefObject } from 'react';
import type { EditorMenuItem } from '../../types/constructor';
import BaseIcon from '../BaseIcon'; import BaseIcon from '../BaseIcon';
import ClickOutside from '../ClickOutside'; import ClickOutside from '../ClickOutside';
import MenuActionButton from './MenuActionButton'; import MenuActionButton from './MenuActionButton';
import PageSelector from './PageSelector'; import PageSelector from './PageSelector';
import type { EditorMenuItem } from '../../types/constructor';
import type { ConstructorToolbarProps } from './types'; import type { ConstructorToolbarProps } from './types';
interface Props { interface Props {

View File

@ -1,7 +1,7 @@
import { mdiChevronLeft, mdiExitToApp } from '@mdi/js'; import { mdiChevronLeft, mdiExitToApp } from '@mdi/js';
import dataFormatter from '../../helpers/dataFormatter';
import BaseButton from '../BaseButton'; import BaseButton from '../BaseButton';
import BaseIcon from '../BaseIcon'; import BaseIcon from '../BaseIcon';
import dataFormatter from '../../helpers/dataFormatter';
interface Props { interface Props {
isSaving: boolean; isSaving: boolean;

View File

@ -5,9 +5,9 @@
* Slug is auto-generated from the page name behind the scenes. * Slug is auto-generated from the page name behind the scenes.
*/ */
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { buildUniqueSlug, sanitizeSlug } from '../../lib/slugHelpers';
import CardBoxModal from '../CardBoxModal'; import CardBoxModal from '../CardBoxModal';
import { sanitizeSlug, buildUniqueSlug } from '../../lib/slugHelpers';
interface CreatePageModalProps { interface CreatePageModalProps {
/** Whether the modal is visible */ /** Whether the modal is visible */

View File

@ -1,8 +1,8 @@
import type { CanvasElement } from '../../types/constructor';
import { import {
StyleSettingsSectionCompact, StyleSettingsSectionCompact,
extractNumericValue, extractNumericValue,
} from '../ElementSettings'; } from '../ElementSettings';
import type { CanvasElement } from '../../types/constructor';
import { buildElementCssPatch } from './elementEditorPanel.helpers'; import { buildElementCssPatch } from './elementEditorPanel.helpers';
interface ElementEditorCommonCssSectionProps { interface ElementEditorCommonCssSectionProps {

View File

@ -1,5 +1,5 @@
import { EffectsSettingsSectionCompact } from '../ElementSettings';
import type { AssetOption, CanvasElement } from '../../types/constructor'; import type { AssetOption, CanvasElement } from '../../types/constructor';
import { EffectsSettingsSectionCompact } from '../ElementSettings';
interface ElementEditorEffectsTabProps { interface ElementEditorEffectsTabProps {
selectedElement: CanvasElement; selectedElement: CanvasElement;

View File

@ -1,5 +1,5 @@
import { GallerySectionStyleInputs } from '../ElementSettings';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import { GallerySectionStyleInputs } from '../ElementSettings';
interface ElementEditorGalleryCssSectionProps { interface ElementEditorGalleryCssSectionProps {
selectedElement: CanvasElement; selectedElement: CanvasElement;

View File

@ -1,28 +1,3 @@
import {
CommonSettingsSectionCompact,
DescriptionSettingsSectionCompact,
MediaSettingsSectionCompact,
GallerySettingsSectionCompact,
CarouselSettingsSectionCompact,
GalleryCarouselSettingsSectionCompact,
InfoPanelSettingsSectionCompact,
} from '../ElementSettings';
import NavigationSettingsSectionCompact from '../ElementSettings/NavigationSettingsSectionCompact';
import {
normalizeAppearDelaySec,
normalizeAppearDurationSec,
} from '../../lib/elementDefaults';
import {
isNavigationElementType,
isDescriptionElementType,
isGalleryElementType,
isCarouselElementType,
isMediaElementType,
isVideoPlayerElementType,
isInfoPanelElementType,
} from '../../lib/elementTypeGuards';
import type { AssetOption, CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import type { import type {
CarouselSlideOperations, CarouselSlideOperations,
GalleryCardOperations, GalleryCardOperations,
@ -30,6 +5,31 @@ import type {
InfoPanelSectionOperations, InfoPanelSectionOperations,
NavigationElementType, NavigationElementType,
} from '../../context/ConstructorContext'; } from '../../context/ConstructorContext';
import {
normalizeAppearDelaySec,
normalizeAppearDurationSec,
} from '../../lib/elementDefaults';
import {
isCarouselElementType,
isDescriptionElementType,
isGalleryElementType,
isInfoPanelElementType,
isMediaElementType,
isNavigationElementType,
isVideoPlayerElementType,
} from '../../lib/elementTypeGuards';
import type { AssetOption, CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import {
CarouselSettingsSectionCompact,
CommonSettingsSectionCompact,
DescriptionSettingsSectionCompact,
GalleryCarouselSettingsSectionCompact,
GallerySettingsSectionCompact,
InfoPanelSettingsSectionCompact,
MediaSettingsSectionCompact,
} from '../ElementSettings';
import NavigationSettingsSectionCompact from '../ElementSettings/NavigationSettingsSectionCompact';
interface ElementEditorGeneralTabProps { interface ElementEditorGeneralTabProps {
selectedElement: CanvasElement; selectedElement: CanvasElement;

View File

@ -9,33 +9,31 @@
import React from 'react'; import React from 'react';
import { import {
useConstructorContext,
useConstructorElements,
useConstructorBackground,
useConstructorAssets, useConstructorAssets,
useConstructorBackground,
useConstructorCollectionOps, useConstructorCollectionOps,
useConstructorDuration, useConstructorDuration,
useConstructorNavigation,
useConstructorEditorTab, useConstructorEditorTab,
useConstructorElements,
useConstructorMenu, useConstructorMenu,
useConstructorNavigation,
} from '../../context/ConstructorContext'; } from '../../context/ConstructorContext';
import {
isGalleryElementType,
isInfoPanelElementType,
} from '../../lib/elementTypeGuards';
import type { SystemUiControlSettings } from '../../types/uiControls';
import { import {
ElementSettingsTabsCompact, ElementSettingsTabsCompact,
GallerySectionStyleInputs, GallerySectionStyleInputs,
InfoPanelStyleInputs, InfoPanelStyleInputs,
} from '../ElementSettings'; } from '../ElementSettings';
import ElementEditorHeader from './ElementEditorHeader';
import {
isGalleryElementType,
isCarouselElementType,
isInfoPanelElementType,
} from '../../lib/elementTypeGuards';
import type { SystemUiControlSettings } from '../../types/uiControls';
import { ElementEditorBackgroundSettings } from './ElementEditorBackgroundSettings'; import { ElementEditorBackgroundSettings } from './ElementEditorBackgroundSettings';
import { ElementEditorCommonCssSection } from './ElementEditorCommonCssSection'; import { ElementEditorCommonCssSection } from './ElementEditorCommonCssSection';
import { ElementEditorEffectsTab } from './ElementEditorEffectsTab'; import { ElementEditorEffectsTab } from './ElementEditorEffectsTab';
import { ElementEditorGalleryCssSection } from './ElementEditorGalleryCssSection'; import { ElementEditorGalleryCssSection } from './ElementEditorGalleryCssSection';
import { ElementEditorGeneralTab } from './ElementEditorGeneralTab'; import { ElementEditorGeneralTab } from './ElementEditorGeneralTab';
import ElementEditorHeader from './ElementEditorHeader';
import { InfoPanelMediaStyleSection } from './InfoPanelMediaStyleSection'; import { InfoPanelMediaStyleSection } from './InfoPanelMediaStyleSection';
import { InfoPanelTextStyleSection } from './InfoPanelTextStyleSection'; import { InfoPanelTextStyleSection } from './InfoPanelTextStyleSection';
import { SystemControlSettingsEditor } from './SystemControlSettingsEditor'; import { SystemControlSettingsEditor } from './SystemControlSettingsEditor';
@ -74,7 +72,6 @@ export function ElementEditorPanel({
// Get state from context // Get state from context
const { const {
selectedElement, selectedElement,
selectedElementId,
updateSelectedElement, updateSelectedElement,
removeSelectedElement, removeSelectedElement,
selectedSystemControl, selectedSystemControl,

View File

@ -1,5 +1,5 @@
import type { CanvasElement } from '../../types/constructor';
import { FONT_OPTIONS } from '../../lib/fonts'; import { FONT_OPTIONS } from '../../lib/fonts';
import type { CanvasElement } from '../../types/constructor';
interface InfoPanelMediaStyleSectionProps { interface InfoPanelMediaStyleSectionProps {
selectedElement: CanvasElement; selectedElement: CanvasElement;

View File

@ -1,5 +1,5 @@
import type { CanvasElement } from '../../types/constructor';
import { FONT_OPTIONS } from '../../lib/fonts'; import { FONT_OPTIONS } from '../../lib/fonts';
import type { CanvasElement } from '../../types/constructor';
type TextAlignValue = 'left' | 'center' | 'right'; type TextAlignValue = 'left' | 'center' | 'right';

View File

@ -1,13 +1,15 @@
import {
getSystemControlAnchorBounds,
type SystemUiControlSettings,
} from '../../types/uiControls';
import type { AssetOption } from '../../types/constructor';
import { addFallbackAssetOption } from '../../lib/constructorHelpers'; import { addFallbackAssetOption } from '../../lib/constructorHelpers';
import { import {
opacityToPercentInput, opacityToPercentInput,
percentInputToOpacityNumber, percentInputToOpacityNumber,
} from '../../lib/opacityPercent'; } from '../../lib/opacityPercent';
import type { AssetOption } from '../../types/constructor';
import {
DEFAULT_UI_CONTROL_SETTINGS,
getSystemControlAnchorBounds,
type SystemUiControlAnchor,
type SystemUiControlSettings,
} from '../../types/uiControls';
interface SystemControlSettingsEditorProps { interface SystemControlSettingsEditorProps {
settings: SystemUiControlSettings; settings: SystemUiControlSettings;
@ -61,9 +63,13 @@ export function SystemControlSettingsEditor({
canvasAspectRatio, canvasAspectRatio,
onChange, onChange,
}: SystemControlSettingsEditorProps) { }: SystemControlSettingsEditorProps) {
const effectiveSettings = {
...DEFAULT_UI_CONTROL_SETTINGS.fullscreen,
...settings,
};
const bounds = getSystemControlAnchorBounds( const bounds = getSystemControlAnchorBounds(
settings.anchor, effectiveSettings.anchor,
settings.buttonSizePercent, effectiveSettings.buttonSizePercent,
canvasAspectRatio, canvasAspectRatio,
); );
@ -79,7 +85,7 @@ export function SystemControlSettingsEditor({
min={bounds.minX} min={bounds.minX}
max={bounds.maxX} max={bounds.maxX}
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={settings.xPercent} value={effectiveSettings.xPercent}
onChange={(event) => onChange={(event) =>
onChange({ onChange({
xPercent: clamp( xPercent: clamp(
@ -100,7 +106,7 @@ export function SystemControlSettingsEditor({
min={bounds.minY} min={bounds.minY}
max={bounds.maxY} max={bounds.maxY}
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={settings.yPercent} value={effectiveSettings.yPercent}
onChange={(event) => onChange={(event) =>
onChange({ onChange({
yPercent: clamp( yPercent: clamp(
@ -118,25 +124,24 @@ export function SystemControlSettingsEditor({
</label> </label>
<select <select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={settings.anchor} value={effectiveSettings.anchor}
onChange={(event) => { onChange={(event) => {
const anchor = event.target const anchor = event.target.value as SystemUiControlAnchor;
.value as SystemUiControlSettings['anchor'];
const nextBounds = getSystemControlAnchorBounds( const nextBounds = getSystemControlAnchorBounds(
anchor, anchor,
settings.buttonSizePercent, effectiveSettings.buttonSizePercent,
canvasAspectRatio, canvasAspectRatio,
); );
onChange({ onChange({
anchor, anchor,
xPercent: clamp( xPercent: clamp(
settings.xPercent, effectiveSettings.xPercent,
nextBounds.minX, nextBounds.minX,
nextBounds.maxX, nextBounds.maxX,
), ),
yPercent: clamp( yPercent: clamp(
settings.yPercent, effectiveSettings.yPercent,
nextBounds.minY, nextBounds.minY,
nextBounds.maxY, nextBounds.maxY,
), ),
@ -237,13 +242,13 @@ export function SystemControlSettingsEditor({
<input <input
type='number' type='number'
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={settings[key]} value={effectiveSettings[key]}
onChange={(event) => { onChange={(event) => {
const value = Number(event.target.value) || 0; const value = Number(event.target.value) || 0;
if (key === 'buttonSizePercent') { if (key === 'buttonSizePercent') {
const nextBounds = getSystemControlAnchorBounds( const nextBounds = getSystemControlAnchorBounds(
settings.anchor, effectiveSettings.anchor,
value, value,
canvasAspectRatio, canvasAspectRatio,
); );
@ -251,12 +256,12 @@ export function SystemControlSettingsEditor({
onChange({ onChange({
buttonSizePercent: value, buttonSizePercent: value,
xPercent: clamp( xPercent: clamp(
settings.xPercent, effectiveSettings.xPercent,
nextBounds.minX, nextBounds.minX,
nextBounds.maxX, nextBounds.maxX,
), ),
yPercent: clamp( yPercent: clamp(
settings.yPercent, effectiveSettings.yPercent,
nextBounds.minY, nextBounds.minY,
nextBounds.maxY, nextBounds.maxY,
), ),

View File

@ -14,7 +14,7 @@
* - Then hides instantly (no CSS transition) since last video frame = new bg * - Then hides instantly (no CSS transition) since last video frame = new bg
*/ */
import React, { useState, useEffect, useCallback } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import CanvasLoadingSpinner from '../CanvasLoadingSpinner'; import CanvasLoadingSpinner from '../CanvasLoadingSpinner';
interface TransitionPreviewOverlayProps { interface TransitionPreviewOverlayProps {

View File

@ -1,11 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { CanvasElement } from '../../types/constructor';
import { import {
removeConstructorContextElementById, removeConstructorContextElementById,
updateConstructorContextElementById, updateConstructorContextElementById,
} from './constructorContextValue.helpers'; } from './constructorContextValue.helpers';
import type { CanvasElement } from '../../types/constructor';
const elements = [ const elements = [
{ id: 'first', type: 'description', label: 'First', xPercent: 10 }, { id: 'first', type: 'description', label: 'First', xPercent: 10 },

View File

@ -1,7 +1,7 @@
import type { CanvasElement } from '../../types/constructor';
import type { TourPage, User } from '../../types/entities';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import { presentationHasAudio } from '../../lib/presentationAudio'; import { presentationHasAudio } from '../../lib/presentationAudio';
import type { CanvasElement } from '../../types/constructor';
import type { TourPage, User } from '../../types/entities';
export const getConstructorPageElementsListHref = (projectId: string) => export const getConstructorPageElementsListHref = (projectId: string) =>
projectId projectId

View File

@ -1,12 +1,12 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { CanvasElement } from '../../types/constructor';
import { import {
getCarouselButtonPositionPatch, getCarouselButtonPositionPatch,
getGalleryCarouselButtonPositionPatch, getGalleryCarouselButtonPositionPatch,
updateElementById, updateElementById,
} from './constructorGalleryCarousel.helpers'; } from './constructorGalleryCarousel.helpers';
import type { CanvasElement } from '../../types/constructor';
test('getGalleryCarouselButtonPositionPatch maps overlay buttons to their coordinate fields', () => { test('getGalleryCarouselButtonPositionPatch maps overlay buttons to their coordinate fields', () => {
assert.deepEqual(getGalleryCarouselButtonPositionPatch('prev', 10, 20), { assert.deepEqual(getGalleryCarouselButtonPositionPatch('prev', 10, 20), {

View File

@ -1,11 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { CanvasElement } from '../../types/constructor';
import { import {
applyNavigationTransitionDurations, applyNavigationTransitionDurations,
getConstructorDurationNotes, getConstructorDurationNotes,
} from './constructorMediaDuration.helpers'; } from './constructorMediaDuration.helpers';
import type { CanvasElement } from '../../types/constructor';
test('getConstructorDurationNotes maps media notes into editor shape', () => { test('getConstructorDurationNotes maps media notes into editor shape', () => {
assert.deepEqual( assert.deepEqual(

View File

@ -5,7 +5,7 @@ export const resolveConstructorPreloadedUrl = ({
}: { }: {
url: string | undefined; url: string | undefined;
getReadyBlobUrl: (url: string) => string | null; getReadyBlobUrl: (url: string) => string | null;
resolveAssetUrl: (url: string) => string; resolveAssetUrl: (url: string | undefined) => string;
}) => { }) => {
if (!url) return ''; if (!url) return '';

View File

@ -1,14 +1,17 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import type { TransitionPreviewState } from '../../types/presentation';
import { import {
areConstructorElementIconsReady, areConstructorElementIconsReady,
buildConstructorTransitionPlaybackConfig,
buildConstructorIconPreloadTargets, buildConstructorIconPreloadTargets,
getDeleteFallbackPageId, buildConstructorTransitionPlaybackConfig,
getConstructorBackgroundSources, getConstructorBackgroundSources,
getConstructorEditorTitle, getConstructorEditorTitle,
getConstructorNavigationErrorMessage, getConstructorNavigationErrorMessage,
getDeleteFallbackPageId,
getExistingPageSlugs, getExistingPageSlugs,
getFirstQueryParam, getFirstQueryParam,
getLastProjectSaveAt, getLastProjectSaveAt,
@ -18,9 +21,6 @@ import {
shouldRenderConstructorCanvasElement, shouldRenderConstructorCanvasElement,
sortTourPagesForDisplay, sortTourPagesForDisplay,
} from './constructorPage.helpers'; } from './constructorPage.helpers';
import type { TourPage } from '../../types/entities';
import type { CanvasElement } from '../../types/constructor';
import type { TransitionPreviewState } from '../../types/presentation';
const page = ( const page = (
id: string, id: string,

View File

@ -1,12 +1,12 @@
import type { TourPage } from '../../types/entities'; import type { TransitionConfig } from '../../hooks/useTransitionPlayback';
import type { import type {
CanvasElement, CanvasElement,
CanvasElementType, CanvasElementType,
EditorMenuItem, EditorMenuItem,
} from '../../types/constructor'; } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import type { TransitionPreviewState } from '../../types/presentation'; import type { TransitionPreviewState } from '../../types/presentation';
import type { SystemUiControlType } from '../../types/uiControls'; import type { SystemUiControlType } from '../../types/uiControls';
import type { TransitionConfig } from '../../hooks/useTransitionPlayback';
export type ConstructorInteractionMode = 'edit' | 'interact'; export type ConstructorInteractionMode = 'edit' | 'interact';
@ -125,7 +125,7 @@ export const getDeleteFallbackPageId = ({
export const buildConstructorIconPreloadTargets = ( export const buildConstructorIconPreloadTargets = (
elements: CanvasElement[], elements: CanvasElement[],
resolveUrl: (url: string) => string, resolveUrl: (url: string | undefined) => string,
) => { ) => {
const urls = elements const urls = elements
.filter( .filter(
@ -146,7 +146,7 @@ export const isConstructorElementIconReady = ({
}: { }: {
element: CanvasElement; element: CanvasElement;
preloadedIconUrlMap: Record<string, boolean>; preloadedIconUrlMap: Record<string, boolean>;
resolveUrl: (url: string) => string; resolveUrl: (url: string | undefined) => string;
}) => { }) => {
const isPreloadableIconElement = const isPreloadableIconElement =
ICON_PRELOADABLE_TYPES.includes(element.type) && Boolean(element.iconUrl); ICON_PRELOADABLE_TYPES.includes(element.type) && Boolean(element.iconUrl);
@ -166,7 +166,7 @@ export const areConstructorElementIconsReady = ({
}: { }: {
elements: CanvasElement[]; elements: CanvasElement[];
preloadedIconUrlMap: Record<string, boolean>; preloadedIconUrlMap: Record<string, boolean>;
resolveUrl: (url: string) => string; resolveUrl: (url: string | undefined) => string;
}) => }) =>
elements.every((element) => elements.every((element) =>
isConstructorElementIconReady({ isConstructorElementIconReady({
@ -214,7 +214,7 @@ export const getConstructorBackgroundSources = ({
embedUrl: string; embedUrl: string;
audioUrl: string; audioUrl: string;
}; };
resolveUrl: (url: string) => string; resolveUrl: (url: string | undefined) => string;
}) => ({ }) => ({
imageUrl: imageUrl:
isEditMode && edit.imageUrl isEditMode && edit.imageUrl
@ -243,7 +243,7 @@ export const buildConstructorTransitionPlaybackConfig = ({
transitionPreview: TransitionPreviewState | null; transitionPreview: TransitionPreviewState | null;
isVideoElementReady: boolean; isVideoElementReady: boolean;
pendingNavigationPageId: string; pendingNavigationPageId: string;
resolveUrl: (url: string) => string; resolveUrl: (url: string | undefined) => string;
}): TransitionConfig | null => { }): TransitionConfig | null => {
if (!transitionPreview || !isVideoElementReady) { if (!transitionPreview || !isVideoElementReady) {
return null; return null;

View File

@ -1,6 +1,6 @@
import { buildUniqueSlug } from '../../lib/slugHelpers';
import type { EditorMenuItem } from '../../types/constructor'; import type { EditorMenuItem } from '../../types/constructor';
import type { TourPage } from '../../types/entities'; import type { TourPage } from '../../types/entities';
import { buildUniqueSlug } from '../../lib/slugHelpers';
import { import {
getDeleteFallbackPageId, getDeleteFallbackPageId,
getReorderedPageIds, getReorderedPageIds,

View File

@ -1,13 +1,13 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import { import {
normalizeForcedNavigationElementTypes, normalizeForcedNavigationElementTypes,
shouldInitializeConstructorPageBackground, shouldInitializeConstructorPageBackground,
toConstructorNavigablePage, toConstructorNavigablePage,
} from './constructorPageNavigationLifecycle.helpers'; } from './constructorPageNavigationLifecycle.helpers';
import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
test('toConstructorNavigablePage keeps only navigation background fields', () => { test('toConstructorNavigablePage keeps only navigation background fields', () => {
assert.deepEqual( assert.deepEqual(

View File

@ -1,8 +1,8 @@
import type { NavigationElementType } from '../../context/ConstructorContext';
import type { NavigablePage } from '../../hooks/usePageNavigationState';
import { isNavigationElementType } from '../../lib/elementTypeGuards'; import { isNavigationElementType } from '../../lib/elementTypeGuards';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities'; import type { TourPage } from '../../types/entities';
import type { NavigationElementType } from '../../context/ConstructorContext';
import type { NavigablePage } from '../../hooks/usePageNavigationState';
export const toConstructorNavigablePage = ( export const toConstructorNavigablePage = (
page: TourPage | null, page: TourPage | null,

View File

@ -1,10 +1,10 @@
import { getNavigationButtonKind } from '../../lib/elementDefaultConstants';
import { import {
mergeElementWithDefaults, mergeElementWithDefaults,
normalizeAppearDelaySec, normalizeAppearDelaySec,
normalizeAppearDurationSec, normalizeAppearDurationSec,
} from '../../lib/elementDefaults'; } from '../../lib/elementDefaults';
import { clamp, createLocalId } from '../../lib/elementInstance.helpers'; import { clamp, createLocalId } from '../../lib/elementInstance.helpers';
import { getNavigationButtonKind } from '../../lib/elementDefaultConstants';
import { import {
isNavigationElementType, isNavigationElementType,
isVideoPlayerElementType, isVideoPlayerElementType,

View File

@ -1,14 +1,14 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import {
buildUpdatedSystemControlSettings,
clampConstructorSystemControlPosition,
} from './constructorSystemControls.helpers';
import { import {
DEFAULT_UI_CONTROL_SETTINGS, DEFAULT_UI_CONTROL_SETTINGS,
type ResolvedUiControlsSettings, type ResolvedUiControlsSettings,
} from '../../types/uiControls'; } from '../../types/uiControls';
import {
buildUpdatedSystemControlSettings,
clampConstructorSystemControlPosition,
} from './constructorSystemControls.helpers';
test('buildUpdatedSystemControlSettings merges a control patch without dropping existing controls', () => { test('buildUpdatedSystemControlSettings merges a control patch without dropping existing controls', () => {
const next = buildUpdatedSystemControlSettings({ const next = buildUpdatedSystemControlSettings({

View File

@ -1,11 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { TransitionPreviewState } from '../../types/presentation';
import { import {
getConstructorTransitionPreviewBuffering, getConstructorTransitionPreviewBuffering,
shouldCloseConstructorTransitionPreview, shouldCloseConstructorTransitionPreview,
} from './constructorTransitionPreviewPlayback.helpers'; } from './constructorTransitionPreviewPlayback.helpers';
import type { TransitionPreviewState } from '../../types/presentation';
const preview = { const preview = {
videoUrl: 'assets/transition.mp4', videoUrl: 'assets/transition.mp4',

View File

@ -5,19 +5,19 @@
*/ */
import type { import type {
AssetOption,
CanvasElement, CanvasElement,
CanvasElementType, CanvasElementType,
ConstructorAsset,
AssetOption,
GalleryCard,
CarouselSlide, CarouselSlide,
NavigationButtonKind, ConstructorAsset,
EditorMenuItem, EditorMenuItem,
EditorTab, EditorTab,
GalleryCard,
NavigationButtonKind,
} from '../../types/constructor'; } from '../../types/constructor';
import type { import type {
PageBackgroundVideoSettings,
PageBackgroundAudioSettings, PageBackgroundAudioSettings,
PageBackgroundVideoSettings,
} from '../../types/pageBackground'; } from '../../types/pageBackground';
/** /**
@ -313,11 +313,11 @@ export interface TransitionPreviewOverlayProps {
// Re-export types from constructor.ts // Re-export types from constructor.ts
export type { export type {
AssetOption,
CanvasElement, CanvasElement,
CanvasElementType, CanvasElementType,
ConstructorAsset,
AssetOption,
GalleryCard,
CarouselSlide, CarouselSlide,
ConstructorAsset,
GalleryCard,
NavigationButtonKind, NavigationButtonKind,
}; };

View File

@ -5,6 +5,7 @@ import {
useEffect, useEffect,
useRef, useRef,
} from 'react'; } from 'react';
import { ELEMENT_TYPE_LABELS } from '../../lib/elementDefaultConstants';
import { parseJsonObject } from '../../lib/parseJson'; import { parseJsonObject } from '../../lib/parseJson';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities'; import type { TourPage } from '../../types/entities';
@ -18,7 +19,6 @@ import {
type ElementDefaultsByType, type ElementDefaultsByType,
type RawConstructorSchema, type RawConstructorSchema,
} from './constructorSchema.helpers'; } from './constructorSchema.helpers';
import { ELEMENT_TYPE_LABELS } from '../../lib/elementDefaultConstants';
export function useActiveConstructorPageHydration({ export function useActiveConstructorPageHydration({
activePage, activePage,

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import type { NextRouter } from 'next/router'; import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useConstructorAuthRedirect({ export function useConstructorAuthRedirect({
router, router,

View File

@ -1,26 +1,26 @@
import { flushSync } from 'react-dom';
import { useCallback, type MouseEvent, type PointerEvent } from 'react'; import { useCallback, type MouseEvent, type PointerEvent } from 'react';
import type { CanvasElement, EditorMenuItem } from '../../types/constructor'; import { flushSync } from 'react-dom';
import type { TourPage } from '../../types/entities';
import type { NavigationContext } from '../../lib/navigationHelpers';
import type {
SystemUiControlSettings,
SystemUiControlType,
} from '../../types/uiControls';
import type { ElementTransitionSettings } from '../../types/transition';
import type { NavigationElementType } from '../../context/ConstructorContext'; import type { NavigationElementType } from '../../context/ConstructorContext';
import { resolveAssetPlaybackUrl } from '../../lib/assetUrl';
import { isElementFlagEnabled } from '../../lib/elementFlags';
import { isNavigationElementType } from '../../lib/elementTypeGuards';
import { logger } from '../../lib/logger';
import type { NavigationContext } from '../../lib/navigationHelpers';
import { import {
getNavigationDirection, getNavigationDirection,
hasPlayableTransition, hasPlayableTransition,
isBackNavigation, isBackNavigation,
resolveNavigationTarget, resolveNavigationTarget,
} from '../../lib/navigationHelpers'; } from '../../lib/navigationHelpers';
import { isNavigationElementType } from '../../lib/elementTypeGuards';
import { isElementFlagEnabled } from '../../lib/elementFlags';
import { resolveAssetPlaybackUrl } from '../../lib/assetUrl';
import { downloadManager } from '../../lib/offline/DownloadManager'; import { downloadManager } from '../../lib/offline/DownloadManager';
import { logger } from '../../lib/logger'; import type { CanvasElement, EditorMenuItem } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import type { ElementTransitionSettings } from '../../types/transition';
import { extractElementTransitionSettings } from '../../types/transition'; import { extractElementTransitionSettings } from '../../types/transition';
import type {
ResolvedUiControlsSettings,
SystemUiControlType,
} from '../../types/uiControls';
import { getConstructorNavigationErrorMessage } from './constructorPage.helpers'; import { getConstructorNavigationErrorMessage } from './constructorPage.helpers';
type ConstructorCanvasInteractionsOptions = { type ConstructorCanvasInteractionsOptions = {
@ -35,10 +35,7 @@ type ConstructorCanvasInteractionsOptions = {
downlink?: number; downlink?: number;
rtt?: number; rtt?: number;
}; };
resolvedUiControlsSettings: Record< resolvedUiControlsSettings: ResolvedUiControlsSettings;
SystemUiControlType,
SystemUiControlSettings
>;
getNavigationContext: () => NavigationContext; getNavigationContext: () => NavigationContext;
getReadyTransitionBlobUrl: (url: string) => string | null; getReadyTransitionBlobUrl: (url: string) => string | null;
selectElementForEdit: (elementId: string) => void; selectElementForEdit: (elementId: string) => void;

View File

@ -2,10 +2,6 @@ import { useMemo } from 'react';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import type { TourPage, User } from '../../types/entities'; import type { TourPage, User } from '../../types/entities';
import {
getExistingPageSlugs,
getLastProjectSaveAt,
} from './constructorPage.helpers';
import { import {
canCurrentUserDeleteConstructorPage, canCurrentUserDeleteConstructorPage,
getConstructorPageElementsListHref, getConstructorPageElementsListHref,
@ -13,6 +9,10 @@ import {
hasConstructorFullWidthCarousel, hasConstructorFullWidthCarousel,
hasConstructorPresentationAudio, hasConstructorPresentationAudio,
} from './constructorDerivedState.helpers'; } from './constructorDerivedState.helpers';
import {
getExistingPageSlugs,
getLastProjectSaveAt,
} from './constructorPage.helpers';
type UseConstructorDerivedStateOptions = { type UseConstructorDerivedStateOptions = {
projectId: string; projectId: string;

View File

@ -6,6 +6,7 @@ import {
type SetStateAction, type SetStateAction,
} from 'react'; } from 'react';
import type { CanvasElement } from '../../types/constructor';
import type { ActiveConstructorGalleryCarousel } from './ConstructorRuntimeOverlays'; import type { ActiveConstructorGalleryCarousel } from './ConstructorRuntimeOverlays';
import { import {
getCarouselButtonPositionPatch, getCarouselButtonPositionPatch,
@ -14,7 +15,6 @@ import {
type CarouselButton, type CarouselButton,
type GalleryCarouselButton, type GalleryCarouselButton,
} from './constructorGalleryCarousel.helpers'; } from './constructorGalleryCarousel.helpers';
import type { CanvasElement } from '../../types/constructor';
export const useConstructorGalleryCarousel = ({ export const useConstructorGalleryCarousel = ({
elements, elements,

View File

@ -1,9 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
buildInfoPanelGalleryState,
getExternalHref,
getInfoPanelBackgroundSources,
} from '../UiElements/infoPanelMedia.helpers';
import { findDefaultOpenInfoPanelElements } from '../../lib/elementFlags'; import { findDefaultOpenInfoPanelElements } from '../../lib/elementFlags';
import { isInfoPanelElementType } from '../../lib/elementTypeGuards'; import { isInfoPanelElementType } from '../../lib/elementTypeGuards';
import type { import type {
@ -11,8 +6,13 @@ import type {
EditorMenuItem, EditorMenuItem,
GalleryCarouselMediaItem, GalleryCarouselMediaItem,
} from '../../types/constructor'; } from '../../types/constructor';
import type { InfoPanelImage } from '../../types/infoPanel';
import type { TourPage } from '../../types/entities'; import type { TourPage } from '../../types/entities';
import type { InfoPanelImage } from '../../types/infoPanel';
import {
buildInfoPanelGalleryState,
getExternalHref,
getInfoPanelBackgroundSources,
} from '../UiElements/infoPanelMedia.helpers';
interface UseConstructorInfoPanelsOptions { interface UseConstructorInfoPanelsOptions {
elements: CanvasElement[]; elements: CanvasElement[];
@ -26,7 +26,7 @@ interface UseConstructorInfoPanelsOptions {
setSelectedMenuItem: (item: EditorMenuItem) => void; setSelectedMenuItem: (item: EditorMenuItem) => void;
setErrorMessage: (message: string) => void; setErrorMessage: (message: string) => void;
currentAudioUrl: string; currentAudioUrl: string;
resolveAssetUrl: (url: string) => string; resolveAssetUrl: (url: string | undefined) => string;
setBackgroundDirectly: ( setBackgroundDirectly: (
imageUrl?: string, imageUrl?: string,
videoUrl?: string, videoUrl?: string,
@ -53,7 +53,7 @@ export function useConstructorInfoPanels({
const defaultInfoPanelPageIdRef = useRef<string | null>(null); const defaultInfoPanelPageIdRef = useRef<string | null>(null);
const [activeInfoPanelIds, setActiveInfoPanelIds] = useState<string[]>([]); const [activeInfoPanelIds, setActiveInfoPanelIds] = useState<string[]>([]);
const [activeDetailImages, setActiveDetailImages] = useState< const [activeDetailImages, setActiveDetailImages] = useState<
Record<string, InfoPanelImage | undefined> Record<string, InfoPanelImage | null | undefined>
>({}); >({});
const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{ const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{
panelId: string; panelId: string;
@ -117,7 +117,7 @@ export function useConstructorInfoPanels({
}, []); }, []);
const setDetailImage = useCallback( const setDetailImage = useCallback(
(panelId: string, image: InfoPanelImage) => { (panelId: string, image: InfoPanelImage | null) => {
setActiveDetailImages((current) => ({ setActiveDetailImages((current) => ({
...current, ...current,
[panelId]: image, [panelId]: image,

View File

@ -3,12 +3,12 @@ import { useCallback, useMemo } from 'react';
import { useIconPreload } from '../../hooks/useIconPreload'; import { useIconPreload } from '../../hooks/useIconPreload';
import type { PreloadCacheProvider } from '../../hooks/video'; import type { PreloadCacheProvider } from '../../hooks/video';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import { resolveConstructorPreloadedUrl } from './constructorMediaPreload.helpers';
import { import {
areConstructorElementIconsReady, areConstructorElementIconsReady,
buildConstructorIconPreloadTargets, buildConstructorIconPreloadTargets,
getConstructorBackgroundSources, getConstructorBackgroundSources,
} from './constructorPage.helpers'; } from './constructorPage.helpers';
import { resolveConstructorPreloadedUrl } from './constructorMediaPreload.helpers';
type ConstructorPreloadOrchestrator = { type ConstructorPreloadOrchestrator = {
getReadyBlob: (url: string) => Blob | null; getReadyBlob: (url: string) => Blob | null;
@ -22,7 +22,7 @@ type UseConstructorMediaPreloadStateOptions = {
isLoading: boolean; isLoading: boolean;
isEditMode: boolean; isEditMode: boolean;
preloadOrchestrator: ConstructorPreloadOrchestrator; preloadOrchestrator: ConstructorPreloadOrchestrator;
resolveAssetUrl: (url: string) => string; resolveAssetUrl: (url: string | undefined) => string;
editBackground: { editBackground: {
imageUrl?: string; imageUrl?: string;
videoUrl?: string; videoUrl?: string;
@ -81,11 +81,7 @@ export function useConstructorMediaPreloadState({
getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl, getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,
resolveAssetUrl, resolveAssetUrl,
}), }),
[ [preloadOrchestrator.getReadyBlobUrl, resolveAssetUrl],
preloadOrchestrator.getReadyBlobUrl,
preloadOrchestrator.readyUrlsVersion,
resolveAssetUrl,
],
); );
const backgroundSources = useMemo( const backgroundSources = useMemo(

View File

@ -1,9 +1,9 @@
import { useCallback, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import type { EditorMenuItem } from '../../types/constructor'; import { useCallback, useState } from 'react';
import type { TourPage } from '../../types/entities';
import { logger } from '../../lib/logger'; import { logger } from '../../lib/logger';
import { queryClient, queryKeys } from '../../lib/queryClient'; import { queryClient, queryKeys } from '../../lib/queryClient';
import type { EditorMenuItem } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import { import {
deleteConstructorPage, deleteConstructorPage,
duplicateConstructorPage, duplicateConstructorPage,
@ -78,7 +78,6 @@ export function useConstructorPageManagement({
}, },
[ [
activePage, activePage,
activePage?.environment,
activePageId, activePageId,
handleReload, handleReload,
isReorderingPages, isReorderingPages,

View File

@ -6,10 +6,10 @@ import {
type SetStateAction, type SetStateAction,
} from 'react'; } from 'react';
import type { NavigationElementType } from '../../context/ConstructorContext';
import type { UsePageNavigationStateResult } from '../../hooks/usePageNavigationState'; import type { UsePageNavigationStateResult } from '../../hooks/usePageNavigationState';
import type { CanvasElement } from '../../types/constructor'; import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities'; import type { TourPage } from '../../types/entities';
import type { NavigationElementType } from '../../context/ConstructorContext';
import { import {
normalizeForcedNavigationElementTypes, normalizeForcedNavigationElementTypes,
shouldInitializeConstructorPageBackground, shouldInitializeConstructorPageBackground,

View File

@ -6,8 +6,8 @@ import type { CanvasElement, EditorMenuItem } from '../../types/constructor';
import type { TourPage } from '../../types/entities'; import type { TourPage } from '../../types/entities';
import type { PageBackgroundState } from '../../types/pageBackground'; import type { PageBackgroundState } from '../../types/pageBackground';
import type { UiControlsSettings } from '../../types/uiControls'; import type { UiControlsSettings } from '../../types/uiControls';
import { useConstructorPageManagement } from './useConstructorPageManagement';
import { shouldRestoreConstructorPageAfterReload } from './constructorPageWorkflow.helpers'; import { shouldRestoreConstructorPageAfterReload } from './constructorPageWorkflow.helpers';
import { useConstructorPageManagement } from './useConstructorPageManagement';
type ConstructorWorkflowProject = { type ConstructorWorkflowProject = {
id?: string; id?: string;
@ -21,7 +21,7 @@ type UseConstructorPageWorkflowOptions = {
project?: ConstructorWorkflowProject | null; project?: ConstructorWorkflowProject | null;
pages: TourPage[]; pages: TourPage[];
activePage: TourPage | null; activePage: TourPage | null;
activePageId: string; activePageId: string | null;
elements: CanvasElement[]; elements: CanvasElement[];
getElements: () => CanvasElement[]; getElements: () => CanvasElement[];
pageBackground: PageBackgroundState; pageBackground: PageBackgroundState;
@ -65,7 +65,9 @@ export function useConstructorPageWorkflow({
pages, pages,
}) })
) { ) {
setActivePageId(currentPageId); if (currentPageId) {
setActivePageId(currentPageId);
}
} }
}, [activePageId, pages, refetchData, setActivePageId]); }, [activePageId, pages, refetchData, setActivePageId]);
@ -83,7 +85,7 @@ export function useConstructorPageWorkflow({
project, project,
pages, pages,
activePage, activePage,
activePageId, activePageId: activePageId || '',
elements, elements,
getElements, getElements,
pageBackground, pageBackground,
@ -99,7 +101,7 @@ export function useConstructorPageWorkflow({
projectId, projectId,
pages, pages,
activePage, activePage,
activePageId, activePageId: activePageId || '',
existingSlugs, existingSlugs,
refetchData, refetchData,
handleReload, handleReload,

View File

@ -1,9 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { AppDispatch } from '../../stores/store';
import { fetch as fetchGlobalTransitionDefaults } from '../../stores/global_transition_defaults/globalTransitionDefaultsSlice'; import { fetch as fetchGlobalTransitionDefaults } from '../../stores/global_transition_defaults/globalTransitionDefaultsSlice';
import { fetch as fetchGlobalUiControlDefaults } from '../../stores/global_ui_control_defaults/globalUiControlDefaultsSlice'; import { fetch as fetchGlobalUiControlDefaults } from '../../stores/global_ui_control_defaults/globalUiControlDefaultsSlice';
import { fetchByProjectAndEnv as fetchProjectTransitionSettings } from '../../stores/project_transition_settings/projectTransitionSettingsSlice'; import { fetchByProjectAndEnv as fetchProjectTransitionSettings } from '../../stores/project_transition_settings/projectTransitionSettingsSlice';
import { fetchByProjectAndEnv as fetchProjectUiControlSettings } from '../../stores/project_ui_control_settings/projectUiControlSettingsSlice'; import { fetchByProjectAndEnv as fetchProjectUiControlSettings } from '../../stores/project_ui_control_settings/projectUiControlSettingsSlice';
import type { AppDispatch } from '../../stores/store';
interface UseConstructorSettingsFetchOptions { interface UseConstructorSettingsFetchOptions {
dispatch: AppDispatch; dispatch: AppDispatch;

View File

@ -48,7 +48,7 @@ export const useConstructorTransitionPreviewPlayback = ({
resetNavigationToIdle: () => void; resetNavigationToIdle: () => void;
onTransitionEnded: () => void; onTransitionEnded: () => void;
onVideoBufferStateChange: (isBuffering: boolean) => void; onVideoBufferStateChange: (isBuffering: boolean) => void;
resolveUrl: (url: string) => string; resolveUrl: (url: string | undefined) => string;
}) => { }) => {
const transitionVideoRef = useRef<HTMLVideoElement | null>(null); const transitionVideoRef = useRef<HTMLVideoElement | null>(null);
const [isTransitionVideoElementReady, setIsTransitionVideoElementReady] = const [isTransitionVideoElementReady, setIsTransitionVideoElementReady] =

View File

@ -1,15 +1,15 @@
import React from 'react';
import axios from 'axios';
import { import {
GridColDef, GridColDef,
GridRenderCellParams, GridRenderCellParams,
GridSingleSelectColDef, GridSingleSelectColDef,
} from '@mui/x-data-grid'; } from '@mui/x-data-grid';
import axios from 'axios';
import React from 'react';
import dataFormatter from '../../helpers/dataFormatter'; import dataFormatter from '../../helpers/dataFormatter';
import DataGridMultiSelect from '../DataGridMultiSelect';
import ListActionsPopover from '../ListActionsPopover';
import { hasPermission } from '../../helpers/userPermissions'; import { hasPermission } from '../../helpers/userPermissions';
import { logger } from '../../lib/logger'; import { logger } from '../../lib/logger';
import DataGridMultiSelect from '../DataGridMultiSelect';
import ListActionsPopover from '../ListActionsPopover';
export interface ColumnMetadata { export interface ColumnMetadata {
field: string; field: string;
@ -163,9 +163,14 @@ function buildColumn(
singleSelectColumn.type = 'singleSelect'; singleSelectColumn.type = 'singleSelect';
singleSelectColumn.sortable = false; singleSelectColumn.sortable = false;
singleSelectColumn.getOptionValue = (value: { id?: string }) => value?.id; singleSelectColumn.getOptionValue = (value: unknown) =>
singleSelectColumn.getOptionLabel = (value: { label?: string }) => typeof value === 'object' && value !== null && 'id' in value
value?.label; ? String(value.id ?? '')
: String(value ?? '');
singleSelectColumn.getOptionLabel = (value: unknown) =>
typeof value === 'object' && value !== null && 'label' in value
? String(value.label ?? '')
: String(value ?? '');
singleSelectColumn.valueOptions = valueOptions; singleSelectColumn.valueOptions = valueOptions;
singleSelectColumn.valueGetter = ( singleSelectColumn.valueGetter = (
value: { id?: string; label?: string; name?: string } | string | null, value: { id?: string; label?: string; name?: string } | string | null,

View File

@ -1,19 +1,26 @@
import { MenuItem, Select, type SelectChangeEvent } from '@mui/material';
import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid'; import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid';
import React, { useEffect, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { MenuItem, Select } from '@mui/material'; import { useEffect, useState } from 'react';
interface Props { interface Props {
entityName: string; entityName: string;
} }
interface SelectOption {
id: string;
label: string;
}
const DataGridMultiSelect = (props: GridRenderEditCellParams & Props) => { const DataGridMultiSelect = (props: GridRenderEditCellParams & Props) => {
const { id, value, field, entityName } = props; const { id, value, field, entityName } = props;
const apiRef = useGridApiContext(); const apiRef = useGridApiContext();
const [options, setOptions] = useState([]); const [options, setOptions] = useState<SelectOption[]>([]);
async function callApi(entityName: string) { async function callApi(entityName: string) {
const data = await axios(`/${entityName}/autocomplete?limit=50`); const data = await axios.get<SelectOption[]>(
`/${entityName}/autocomplete?limit=50`,
);
return data.data; return data.data;
} }
@ -21,18 +28,24 @@ const DataGridMultiSelect = (props: GridRenderEditCellParams & Props) => {
callApi(entityName).then((data) => { callApi(entityName).then((data) => {
setOptions(data); setOptions(data);
}); });
}, []); }, [entityName]);
const handleChange = (event) => { const handleChange = (event: SelectChangeEvent<unknown>) => {
const eventValue = event.target.value; // The new value entered by the user const eventValue = event.target.value; // The new value entered by the user
const newValue = const newValue =
typeof eventValue === 'string' ? value.split(',') : eventValue; typeof eventValue === 'string'
? eventValue.split(',')
: Array.isArray(eventValue)
? eventValue
: [];
apiRef.current.setEditCellValue({ apiRef.current.setEditCellValue({
id, id,
field, field,
value: newValue.filter((x) => x !== ''), value: newValue.filter(
(x): x is string => typeof x === 'string' && x !== '',
),
}); });
}; };

View File

@ -1,26 +1,26 @@
import React, { ChangeEvent, useEffect, useState } from 'react';
import BaseIcon from './BaseIcon';
import { mdiFileUploadOutline } from '@mdi/js'; import { mdiFileUploadOutline } from '@mdi/js';
import React, { useEffect, useRef, useState } from 'react';
import BaseIcon from './BaseIcon';
type Props = { type Props = {
file: File | null; file: File | null;
setFile: (file: File) => void; setFile: (file: File | null) => void;
formats?: string; formats?: string;
}; };
const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
const [highlight, setHighlight] = useState(false); const [highlight, setHighlight] = useState(false);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const fileInput = React.createRef<HTMLInputElement>(); const fileInput = useRef<HTMLInputElement | null>(null);
useEffect(() => { useEffect(() => {
if (!file && fileInput) fileInput.current.value = ''; if (!file && fileInput.current) fileInput.current.value = '';
}, [file, fileInput]); }, [file, fileInput]);
function onFilesAdded(files: FileList | null) { function onFilesAdded(files: FileList | null) {
if (files && files[0]) { if (files && files[0]) {
const newFile = files[0]; const newFile = files[0];
const fileExtension = newFile.name.split('.').pop().toLowerCase(); const fileExtension = newFile.name.split('.').pop()?.toLowerCase() || '';
if (formats.includes(fileExtension) || !formats) { if (formats.includes(fileExtension) || !formats) {
setFile(newFile); setFile(newFile);
@ -31,7 +31,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
} }
} }
function onDragOver(e) { function onDragOver(e: React.DragEvent<HTMLDivElement>) {
e.preventDefault(); e.preventDefault();
setHighlight(true); setHighlight(true);
} }
@ -40,7 +40,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
setHighlight(false); setHighlight(false);
} }
function onDrop(e) { function onDrop(e: React.DragEvent<HTMLDivElement>) {
e.preventDefault(); e.preventDefault();
const files = e.dataTransfer.files; const files = e.dataTransfer.files;
@ -49,11 +49,6 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
setHighlight(false); setHighlight(false);
} }
const onClear = () => {
setFile(null);
setErrorMessage('');
};
return ( return (
<div <div
className='flex items-center justify-center w-full mb-4' className='flex items-center justify-center w-full mb-4'

View File

@ -5,13 +5,13 @@
* Manages carousel slides with images and captions. * Manages carousel slides with images and captions.
*/ */
import React from 'react';
import { mdiPlus, mdiTrashCan } from '@mdi/js'; import { mdiPlus, mdiTrashCan } from '@mdi/js';
import React from 'react';
import { FONT_OPTIONS } from '../../lib/fonts';
import BaseButton from '../BaseButton'; import BaseButton from '../BaseButton';
import CardBox from '../CardBox'; import CardBox from '../CardBox';
import FormField from '../FormField'; import FormField from '../FormField';
import type { CarouselSettingsSectionProps } from './types'; import type { CarouselSettingsSectionProps } from './types';
import { FONT_OPTIONS } from '../../lib/fonts';
const CarouselSettingsSection: React.FC<CarouselSettingsSectionProps> = ({ const CarouselSettingsSection: React.FC<CarouselSettingsSectionProps> = ({
carouselPrevIconUrl, carouselPrevIconUrl,

View File

@ -6,9 +6,9 @@
*/ */
import React from 'react'; import React from 'react';
import type { CarouselSlide, AssetOption } from '../../types/constructor';
import { addFallbackAssetOption } from '../../lib/constructorHelpers'; import { addFallbackAssetOption } from '../../lib/constructorHelpers';
import { FONT_OPTIONS } from '../../lib/fonts'; import { FONT_OPTIONS } from '../../lib/fonts';
import type { AssetOption, CarouselSlide } from '../../types/constructor';
interface CarouselSettingsSectionCompactProps { interface CarouselSettingsSectionCompactProps {
carouselSlides: CarouselSlide[]; carouselSlides: CarouselSlide[];

Some files were not shown because too many files have changed in this diff Show More