diff --git a/src/main/resources/assets/react/document/Documents.tsx b/src/main/resources/assets/react/document/Documents.tsx index f1847b51..ca266b6f 100644 --- a/src/main/resources/assets/react/document/Documents.tsx +++ b/src/main/resources/assets/react/document/Documents.tsx @@ -26,17 +26,19 @@ import { } from 'semantic-ui-react'; import {TypedReactJson} from '../search/TypedReactJson'; import {HoverPopup} from '../components/HoverPopup'; -import DragAndDropableHeaderCell from './DragAndDropableHeaderCell'; import { COLUMN_NAME_COLLECTION, COLUMN_NAME_DOCUMENT_TYPE, COLUMN_NAME_LANGUAGE, COLUMN_NAME_ID, COLUMN_NAME_JSON, + SELECTED_COLUMNS_DEFAULT +} from './constants'; +import DragAndDropableHeaderCell from './DragAndDropableHeaderCell'; +import { FRAGMENT_SIZE_DEFAULT, POST_TAG, PRE_TAG, - SELECTED_COLUMNS_DEFAULT, useDocumentsState } from './useDocumentsState'; @@ -125,7 +127,7 @@ export function Documents({ queryDocuments, searchedString, // setSearchedString, selectedCollections, setSelectedCollections, - selectedColumnsState, persistSelectedColumns, + selectedColumnsState, setSelectedColumnsState, selectedDocumentTypes, setSelectedDocumentTypes, start, setStart, } = useDocumentsState({ @@ -328,7 +330,7 @@ export function Documents({ {value} ) => { const newSelectedColumns = value as string[]; - persistSelectedColumns(newSelectedColumns); + setSelectedColumnsState(newSelectedColumns); queryDocuments({ collectionsFilter: selectedCollections, documentsTypesFilter: selectedDocumentTypes, @@ -532,16 +534,16 @@ export function Documents({ ? 'Document' : columnName } - key={i} + id={columnName} + index={i} + key={`column-${columnName}`} onDrop={({ - fromIndex, - toIndex + fromId, + toId }) => handleDroppedColumn({ - fromIndex, - selectedColumns: selectedColumnsState, - toIndex + fromId, + toId })} - index={i} />)} {/*columnOptions .filter(({value}) => selectedColumns.includes(value as string)) diff --git a/src/main/resources/assets/react/document/DragAndDropableHeaderCell.tsx b/src/main/resources/assets/react/document/DragAndDropableHeaderCell.tsx index cc8a8997..537f7377 100644 --- a/src/main/resources/assets/react/document/DragAndDropableHeaderCell.tsx +++ b/src/main/resources/assets/react/document/DragAndDropableHeaderCell.tsx @@ -5,7 +5,7 @@ import type { interface Item { - id: number + id: string } @@ -36,13 +36,14 @@ function Overlay({color}: {color: string}) { export default function DragAndDropableHeaderCell( props: StrictTableHeaderCellProps & { + id: string index: number onDrop: ({ - fromIndex, - toIndex + fromId, + toId }: { - fromIndex: number - toIndex: number + fromId: string + toId: string }) => void style?: React.CSSProperties // "missing" from StrictTableHeaderCellProps } @@ -50,7 +51,7 @@ export default function DragAndDropableHeaderCell( const { children, content, - index, + id: idProp, onDrop = () => {/* default no-op dummy just in case */}, style = {}, ...tableHeaderCellPropsExceptChildrenOrContentOrStyle @@ -61,20 +62,22 @@ export default function DragAndDropableHeaderCell( }>(() => ({ type: 'HeaderCell', // canDrag: (monitor)=> { - // console.debug('on canDrag monitor', monitor, 'index', index); + // console.debug('on canDrag monitor', monitor); // return true; // }, collect: monitor => ({ isDragging: !!monitor.isDragging(), }), // end: (item, monitor) => { - // console.debug('on drag end item', item, 'monitor', monitor, 'index', index); + // console.debug('on drag end item', item, 'monitor', monitor); // }, // isDragging: (monitor) => { - // console.debug('on isDragging monitor', monitor, 'index', index); + // console.debug('on isDragging monitor', monitor); // return true; // }, - item: { id: index }, + item: { + id: idProp, + }, // options: { // dropEffect: 'copy', // // dropEffect: 'move', @@ -86,7 +89,13 @@ export default function DragAndDropableHeaderCell( // offsetX: 0, // offsetY: 0, // }, - })); + }), + // A dependency array used for memoization. + // This behaves like the built-in useMemoReact hook. + // The default value is an empty array for function spec, + // and an array containing the spec for an object spec. + // [] + ); // console.debug('dragProps', dragProps); const { isDragging } = dragProps; @@ -97,17 +106,22 @@ export default function DragAndDropableHeaderCell( () => ({ accept: 'HeaderCell', canDrop: (item/*, monitor*/) => { - // console.debug('on canDrop item', item, 'monitor', monitor, 'index', index); - const {id} = item; - return id !== index; + // console.debug('on canDrop item', item, 'idProp', idProp); + // console.debug('on canDrop monitor', monitor); + const { + id, + } = item; + return id !== idProp; }, drop: (item/*, monitor*/) => { - // console.debug('on drop item', item, 'monitor', monitor, 'index', index); - const {id} = item; // The dropped item - // index is the target index + // console.debug('on drop item', item, 'idProp', idProp); + // console.debug('on drop monitor', monitor); + const { + id, + } = item; // The dropped item onDrop({ - fromIndex: id, - toIndex: index + fromId: id, + toId: idProp }); }, collect: (monitor) => ({ @@ -115,12 +129,18 @@ export default function DragAndDropableHeaderCell( isOver: !!monitor.isOver(), }), // hover: (item, monitor) => { - // console.debug('on hover item', item, 'monitor', monitor, 'index', index); + // console.debug('on hover item', item, 'monitor', monitor); // }, - item: { id: index }, + item: { + id: idProp, + }, // options: {} }), - [index] + // A dependency array used for memoization. + // This behaves like the built-in useMemoReact hook. + // The default value is an empty array for function spec, + // and an array containing the spec for an object spec. + // [] ); // console.debug('dropProps', dropProps); const { canDrop, isOver } = dropProps; @@ -135,7 +155,7 @@ export default function DragAndDropableHeaderCell( style={{ ...style, cursor: 'grabbing', - opacity: 0, + opacity: 0.5, }} > { diff --git a/src/main/resources/assets/react/document/constants.ts b/src/main/resources/assets/react/document/constants.ts new file mode 100644 index 00000000..fcf20f0c --- /dev/null +++ b/src/main/resources/assets/react/document/constants.ts @@ -0,0 +1,12 @@ +export const COLUMN_NAME_COLLECTION = '_collection'; +export const COLUMN_NAME_DOCUMENT_TYPE = '_documentType'; +export const COLUMN_NAME_ID = '_id'; +export const COLUMN_NAME_JSON = '_json'; +export const COLUMN_NAME_LANGUAGE = '_language'; +export const SELECTED_COLUMNS_DEFAULT = [ + COLUMN_NAME_JSON, + COLUMN_NAME_ID, + COLUMN_NAME_COLLECTION, + COLUMN_NAME_DOCUMENT_TYPE, + COLUMN_NAME_LANGUAGE, +] as const; diff --git a/src/main/resources/assets/react/document/getColumns.ts b/src/main/resources/assets/react/document/getColumns.ts new file mode 100644 index 00000000..99cbda21 --- /dev/null +++ b/src/main/resources/assets/react/document/getColumns.ts @@ -0,0 +1,40 @@ +import type {JSONResponse} from '../../../services/graphQL/fetchers/index.d'; + +type GetProfileResponse = JSONResponse<{ + getProfile: { + documents: { + columns: string[]|string + } + } +}> + + +import {forceArray} from '@enonic/js-utils'; +import * as gql from 'gql-query-builder'; +import {SELECTED_COLUMNS_DEFAULT} from './constants'; + + +export async function getColumns({ + servicesBaseUrl +}: { + servicesBaseUrl: string +}) { + return fetch(`${servicesBaseUrl}/graphQL`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(gql.query({ + operation: 'getProfile' + })) + }) + .then(res => res.json() as GetProfileResponse) + .then(object => { + // console.log('object', object); + const { + documents: { + columns = [...SELECTED_COLUMNS_DEFAULT] // When there is no profile, use defaults, deref to avoid type issues + } = {} + } = object.data.getProfile || {}; + // console.log('columns', columns); + return forceArray(columns); + }); +} diff --git a/src/main/resources/assets/react/document/persistColumns.ts b/src/main/resources/assets/react/document/persistColumns.ts new file mode 100644 index 00000000..06ceedb1 --- /dev/null +++ b/src/main/resources/assets/react/document/persistColumns.ts @@ -0,0 +1,63 @@ +import type {JSONResponse} from '../../../services/graphQL/fetchers/index.d'; + +type ModifyProfileResponse = JSONResponse<{ + modifyProfile: { + columns?: string[] + } +}> + +import fastDeepEqual from 'fast-deep-equal/react'; +import * as gql from 'gql-query-builder'; + + +export async function persistColumns({ + columns: columnsParam, + getColumns, + servicesBaseUrl +}: { + columns: string[] + getColumns: ({servicesBaseUrl}: {servicesBaseUrl: string}) => Promise + servicesBaseUrl: string +}) { + return getColumns({servicesBaseUrl}).then(prevColumns => { + if (fastDeepEqual(columnsParam, prevColumns)) { + // console.debug('columns unchanged', prevColumns); + return prevColumns; + } else { + // console.debug('columns changed', columnsParam, 'prev', prevColumns); + return fetch(`${servicesBaseUrl}/graphQL`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(gql.mutation({ + operation: 'modifyProfile', + variables: { + object: { + list: false, + required: true, + type: 'JSON', + value: { + columns: columnsParam + } + }, + scope: { + list: false, + required: true, + type: 'String', + value: 'documents' + } + } + })) + }) + .then(res => res.json() as ModifyProfileResponse) + .then((object) => { + // console.log('object', object); + const { + columns// = SELECTED_COLUMNS_DEFAULT // This will reset to default when all columns are removed + } = object.data.modifyProfile; + return columns; + }); + } + }); + + +} diff --git a/src/main/resources/assets/react/document/useDocumentsState.ts b/src/main/resources/assets/react/document/useDocumentsState.ts index 9a726188..525452c0 100644 --- a/src/main/resources/assets/react/document/useDocumentsState.ts +++ b/src/main/resources/assets/react/document/useDocumentsState.ts @@ -3,12 +3,10 @@ import type { DropdownItemProps, PaginationProps, } from 'semantic-ui-react'; -import type {JSONResponse} from '../../../services/graphQL/fetchers/index.d'; import { QUERY_OPERATOR_OR, - forceArray, // storage } from '@enonic/js-utils'; import {useWhenInit} from "@seamusleahy/init-hooks"; @@ -30,6 +28,16 @@ import { } from '../../../services/graphQL/constants'; import {useInterval} from '../utils/useInterval'; import {useUpdateEffect} from '../utils/useUpdateEffect'; +import { + COLUMN_NAME_COLLECTION, + COLUMN_NAME_DOCUMENT_TYPE, + COLUMN_NAME_LANGUAGE, + COLUMN_NAME_ID, + COLUMN_NAME_JSON, + SELECTED_COLUMNS_DEFAULT +} from './constants'; +import {getColumns} from './getColumns'; +import {persistColumns} from './persistColumns'; // const bool = storage.query.dsl.bool; @@ -48,23 +56,11 @@ const FIELD_PATH_META = 'document_metadata' as const; // TODO _meta ? const GQL_INPUT_TYPE_HIGHLIGHT = 'InputTypeHighlight'; -export const COLUMN_NAME_COLLECTION = '_collection'; -export const COLUMN_NAME_DOCUMENT_TYPE = '_documentType'; -export const COLUMN_NAME_ID = '_id'; -export const COLUMN_NAME_JSON = '_json'; -export const COLUMN_NAME_LANGUAGE = '_language'; export const FRAGMENT_SIZE_DEFAULT = 150; const PER_PAGE_DEFAULT = 10; export const POST_TAG = ''; export const PRE_TAG = ''; -export const SELECTED_COLUMNS_DEFAULT = [ - COLUMN_NAME_COLLECTION, - COLUMN_NAME_DOCUMENT_TYPE, - COLUMN_NAME_LANGUAGE, - COLUMN_NAME_ID, - COLUMN_NAME_JSON, -]; const OPTIONS_COLUMNS_DEFAULT: DropdownItemProps[] = [{ text: 'Collection', @@ -81,7 +77,7 @@ const OPTIONS_COLUMNS_DEFAULT: DropdownItemProps[] = [{ },{ text: 'JSON', value: COLUMN_NAME_JSON -}] +}]; export function useDocumentsState({ servicesBaseUrl @@ -540,43 +536,22 @@ export function useDocumentsState({ ) ); setDoing('Getting profile...'); - fetch(`${servicesBaseUrl}/graphQL`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(gql.query({ - operation: 'getProfile' - })) - }) - .then(res => res.json() as JSONResponse<{ - getProfile: { - documents: { - columns: string[] - } - } - }>) - .then(object => { - // console.log('object', object); - const { - documents: { - columns = SELECTED_COLUMNS_DEFAULT // When there is no profile, use defaults - } = {} - } = object.data.getProfile || {}; - // console.log('columns', columns); - const newSelectedColumns = forceArray(columns); - setSelectedColumnsState(JSON.parse(JSON.stringify(newSelectedColumns))); // NOTE: dereffing: trying to make it work with dragNdrop columns - queryDocuments({ - collectionsFilter: selectedCollections, - documentsTypesFilter: selectedDocumentTypes, - fragmentSize, - operator: operatorState, - perPage, - query, - selectedColumns: newSelectedColumns, - start, // Keep start when columns changes - // updateSelectedCollections: true, - // updateSelectedDocumentTypes: true, - }); + getColumns({servicesBaseUrl}).then(columns => { + // console.debug('columns', columns); + setSelectedColumnsState(JSON.parse(JSON.stringify(columns))); // NOTE: dereffing: trying to make it work with dragNdrop columns + queryDocuments({ + collectionsFilter: selectedCollections, + documentsTypesFilter: selectedDocumentTypes, + fragmentSize, + operator: operatorState, + perPage, + query, + selectedColumns: columns, + start, // Keep start when columns changes + // updateSelectedCollections: true, + // updateSelectedDocumentTypes: true, }); + }); }); }, [ // The callback is not executed when it's deps changes. Only the reference to the callback is updated. fragmentSize, @@ -626,76 +601,48 @@ export function useDocumentsState({ const persistSelectedColumns = React.useCallback((newSelectedColumns: string[]) => { DEBUG_DEPENDENCIES && console.debug('persistSelectedColumns callback called'); setDoing('Persisting selected columns...'); - setSelectedColumnsState(JSON.parse(JSON.stringify(newSelectedColumns))); + // setSelectedColumnsState(JSON.parse(JSON.stringify(newSelectedColumns))); setLoading(true); - fetch(`${servicesBaseUrl}/graphQL`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(gql.mutation({ - operation: 'modifyProfile', - variables: { - object: { - list: false, - required: true, - type: 'JSON', - value: { - columns: newSelectedColumns - } - }, - scope: { - list: false, - required: true, - type: 'String', - value: 'documents' - } - } - })) - }) - .then(res => res.json() as JSONResponse<{ - modifyProfile: { - columns: string[] - } - }>) - .then((/*json*/) => { - // console.log('json', json); - // const { - // columns = SELECTED_COLUMNS_DEFAULT // This will reset to default when all columns are removed - // } = json.data.modifyProfile; - // const returnedSelectedColumns = forceArray(columns); - // console.log('returnedSelectedColumns', returnedSelectedColumns); - // // ERROR: For some reason this does play with the dragNdrop columns - // setSelectedColumnsState( - // JSON.parse(JSON.stringify(returnedSelectedColumns)) // Let's see if dereffing will fix it! - // ); - setDoing(''); - setLoading(false); - }); + persistColumns({ + columns: newSelectedColumns, + getColumns, + servicesBaseUrl + }).then((/*columns*/) => { + setDoing(''); + setLoading(false); + }); }, [ // The callback is not executed when it's deps changes. Only the reference to the callback is updated. servicesBaseUrl, ]); - const handleDroppedColumn = React.useCallback(({ - fromIndex, - selectedColumns, - toIndex + function handleDroppedColumn({ + fromId, + toId }: { - fromIndex: number - selectedColumns: string[], - toIndex: number - }) => { - DEBUG_DEPENDENCIES && console.debug('handleDroppedColumn fromIndex', fromIndex, 'toIndex', toIndex, 'selectedColumns', selectedColumns); - console.debug('fromIndex', fromIndex, 'toIndex', toIndex, 'selectedColumns', selectedColumns); - const newSelectedColumns = JSON.parse(JSON.stringify(selectedColumns)) as typeof selectedColumns; // deref was a bad idea? - const element = newSelectedColumns[fromIndex]; - console.debug('newSelectedColumns before', newSelectedColumns, 'element', element); - newSelectedColumns.splice(fromIndex, 1); // Remove 1 element from array - console.debug('newSelectedColumns underway', newSelectedColumns); - newSelectedColumns.splice(toIndex, 0, element); // Insert element at new position - console.debug('newSelectedColumns after', newSelectedColumns); - persistSelectedColumns(newSelectedColumns); - }, [ - persistSelectedColumns - ]); + fromId: string + toId: string + }) { + setSelectedColumnsState(prev => { + const fromIndex = prev.indexOf(fromId); + const toIndex = prev.indexOf(toId); + DEBUG_DEPENDENCIES && console.debug( + 'handleDroppedColumn fromId', fromId, + 'fromIndex', fromIndex, + 'toId', toId, + 'toIndex', toIndex, + 'prev', prev, + // 'selectedColumnsState', selectedColumnsState // WARNING: For some reason this doesn't change !!! + ); + const newSelectedColumns = JSON.parse(JSON.stringify(prev)) as typeof prev; // deref was a bad idea? + const element = newSelectedColumns[fromIndex]; + // console.debug('newSelectedColumns before', newSelectedColumns, 'element', element); + newSelectedColumns.splice(fromIndex, 1); // Remove 1 element from array + // console.debug('newSelectedColumns underway', newSelectedColumns); + newSelectedColumns.splice(toIndex, 0, element); // Insert element at new position + // console.debug('newSelectedColumns after', newSelectedColumns); + return newSelectedColumns; + }); + } //────────────────────────────────────────────────────────────────────────── // Init @@ -718,7 +665,14 @@ export function useDocumentsState({ // Updates (changes, not init) //────────────────────────────────────────────────────────────────────────── useUpdateEffect(() => { - DEBUG_DEPENDENCIES && console.debug('deBouncedFragmentSize updateEffect trigged. deBouncedFragmentSize', deBouncedFragmentSize); + DEBUG_DEPENDENCIES && console.debug('updateEffect[selectedColumnsState] triggered. selectedColumnsState', selectedColumnsState); + persistSelectedColumns(selectedColumnsState); + }, [ + selectedColumnsState + ]) + + useUpdateEffect(() => { + DEBUG_DEPENDENCIES && console.debug('updateEffect[deBouncedFragmentSize] triggered. deBouncedFragmentSize', deBouncedFragmentSize); queryDocuments({ collectionsFilter: selectedCollections, documentsTypesFilter: selectedDocumentTypes, @@ -732,7 +686,6 @@ export function useDocumentsState({ }, [ deBouncedFragmentSize, queryDocuments, - // So, this used the initial perPage, maybe because queryDocuments doesn't depend on perPage? ]); //────────────────────────────────────────────────────────────────────────── @@ -773,7 +726,7 @@ export function useDocumentsState({ queryDocuments, searchedString, // setSearchedString, selectedCollections, setSelectedCollections, - selectedColumnsState, persistSelectedColumns, + selectedColumnsState, setSelectedColumnsState, selectedDocumentTypes, setSelectedDocumentTypes, start, setStart, };