Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 62 additions & 14 deletions src/components/modals/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import * as selection from '../../device/selection'
import globals from '../../globals'
import documentSort from '../../selectors/documentSort'
import exportContext, { exportFilter } from '../../selectors/exportContext'
import { getChildrenRanked } from '../../selectors/getChildren'
import getDescendantThoughtIds from '../../selectors/getDescendantThoughtIds'
import hasMulticursor from '../../selectors/hasMulticursor'
import simplifyPath from '../../selectors/simplifyPath'
Expand Down Expand Up @@ -296,6 +297,8 @@ const ModalExport: FC<{ simplePaths: SimplePath[] }> = ({ simplePaths }) => {
const [shouldIncludeMetaAttributes, setShouldIncludeMetaAttributes] = useState(false)
const [shouldIncludeArchived, setShouldIncludeArchived] = useState(false)
const [shouldIncludeMarkdownFormatting, setShouldIncludeMarkdownFormatting] = useState(true)
const [shouldExportFirstThought, setShouldExportFirstThought] = useState(true)
const [shouldExportSubthoughts, setShouldExportSubthoughts] = useState(true)
const [selected, setSelected] = useState(exportOptions[0])
const [numDescendantsInState, setNumDescendantsInState] = useState<number | null>(null)

Expand Down Expand Up @@ -348,12 +351,20 @@ const ModalExport: FC<{ simplePaths: SimplePath[] }> = ({ simplePaths }) => {
// Sort in document order. At this point, all thoughts are pulled and in state.
const sortedPaths = documentSort(exportedState, simplePaths)

const exported = sortedPaths
.map(simplePath =>
exportContext(exportedState, head(simplePath), selected.type, {
// When shouldExportFirstThought is false, expand each selected path to its children's IDs.
// For single selection this skips the root thought; for multiple selection it skips the entire first level.
const exportIds = !shouldExportFirstThought
? sortedPaths.flatMap(simplePath => getChildrenRanked(exportedState, head(simplePath)).map(child => child.id))
: sortedPaths.map(simplePath => head(simplePath))

const exported = exportIds
.map(thoughtId =>
exportContext(exportedState, thoughtId, selected.type, {
excludeArchived: !shouldIncludeArchived,
excludeMarkdownFormatting: !shouldIncludeMarkdownFormatting,
excludeMeta: !shouldIncludeMetaAttributes,
// maxDepth 0 means only the thought itself with no children
maxDepth: !shouldExportSubthoughts ? 0 : undefined,
}),
)
.join('\n')
Expand Down Expand Up @@ -396,23 +407,32 @@ const ModalExport: FC<{ simplePaths: SimplePath[] }> = ({ simplePaths }) => {

// when exporting HTML, we have to do a full traversal since the numDescendants heuristic of counting the number of lines in the exported content does not work
if (selected.type === 'text/html' && exportedState) {
setNumDescendantsInState(
getDescendantThoughtIds(exportedState, id, {
filterAndTraverse: thought => shouldIncludeMetaAttributes || thought.value !== '=note',
filterFunction: exportFilter({
excludeArchived: !shouldIncludeArchived,
excludeMeta: !shouldIncludeMetaAttributes,
}),
}).length,
)
// When subthoughts are excluded, there are no descendants by definition.
const numDescendants = !shouldExportSubthoughts
? 0
: getDescendantThoughtIds(exportedState, id, {
filterAndTraverse: thought => shouldIncludeMetaAttributes || thought.value !== '=note',
filterFunction: exportFilter({
excludeArchived: !shouldIncludeArchived,
excludeMeta: !shouldIncludeMetaAttributes,
}),
}).length
setNumDescendantsInState(numDescendants)
}

if (!isPulling) {
setExportContentFromCursor()
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[selected, shouldIncludeMetaAttributes, shouldIncludeArchived, shouldIncludeMarkdownFormatting],
[
selected,
shouldIncludeMetaAttributes,
shouldIncludeArchived,
shouldIncludeMarkdownFormatting,
shouldExportFirstThought,
shouldExportSubthoughts,
],
)

useEffect(
Expand Down Expand Up @@ -551,9 +571,31 @@ const ModalExport: FC<{ simplePaths: SimplePath[] }> = ({ simplePaths }) => {
/** Updates archived checkbox value when clicked and set the appropriate value in the selected option. */
const onChangeFormattingCheckbox = () => setShouldIncludeMarkdownFormatting(!shouldIncludeMarkdownFormatting)

/** Toggles whether the first (top-level) thought is included in the export. */
const onChangeExportFirstThoughtCheckbox = () => setShouldExportFirstThought(!shouldExportFirstThought)

/** Toggles whether subthoughts (descendants) are included in the export. */
const onChangeExportSubthoughtsCheckbox = () => setShouldExportSubthoughts(!shouldExportSubthoughts)

/** Created an array of objects so that we can just add object here to get multiple checkbox options created. */
const advancedSettingsArray: AdvancedSetting[] = useMemo(
() => [
{
id: 'exportFirstThought',
onChange: onChangeExportFirstThoughtCheckbox,
checked: shouldExportFirstThought,
title: 'Export first thought',
description:
'When checked, the top-level thought is included in the export. When unchecked, the first thought is skipped and only its descendants are exported. When multiple thoughts are selected, the entire first level is skipped.',
},
{
id: 'exportSubthoughts',
onChange: onChangeExportSubthoughtsCheckbox,
checked: shouldExportSubthoughts,
title: 'Export subthoughts',
description:
'When checked, all subthoughts are included in the export. When unchecked, only the top-level thoughts are exported without any descendants.',
},
{
id: 'meta',
onChange: onChangeMetaCheckbox,
Expand Down Expand Up @@ -583,7 +625,13 @@ const ModalExport: FC<{ simplePaths: SimplePath[] }> = ({ simplePaths }) => {
],

// eslint-disable-next-line react-hooks/exhaustive-deps
[shouldIncludeArchived, shouldIncludeMetaAttributes, shouldIncludeMarkdownFormatting],
[
shouldIncludeArchived,
shouldIncludeMetaAttributes,
shouldIncludeMarkdownFormatting,
shouldExportFirstThought,
shouldExportSubthoughts,
],
)

return (
Expand Down
47 changes: 47 additions & 0 deletions src/selectors/__tests__/exportContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,50 @@
- b
- c`)
})

it('maxDepth 0 exports only the root thought with no children', () => {
const text = `
- a
- b
- c
`

const steps = [importText({ text }), setCursor(['a'])]
const stateNew = reducerFlow(steps)(initialState())
const exported = exportContext(stateNew, ['a'], 'text/plain', { maxDepth: 0 })

expect(exported).toBe(`- a`)

Check failure on line 256 in src/selectors/__tests__/exportContext.ts

View workflow job for this annotation

GitHub Actions / TDD — Unit tests

[unit] src/selectors/__tests__/exportContext.ts > maxDepth 0 exports only the root thought with no children

AssertionError: expected '- a\n - b\n - c' to be '- a' // Object.is equality - Expected + Received - a + - b + - c ❯ src/selectors/__tests__/exportContext.ts:256:20
})

it('maxDepth 1 exports the root thought and its direct children only', () => {
const text = `
- a
- b
- c
- d
- e
`

const steps = [importText({ text }), setCursor(['a'])]
const stateNew = reducerFlow(steps)(initialState())
const exported = exportContext(stateNew, ['a'], 'text/plain', { maxDepth: 1 })

expect(exported).toBe(`- a

Check failure on line 272 in src/selectors/__tests__/exportContext.ts

View workflow job for this annotation

GitHub Actions / TDD — Unit tests

[unit] src/selectors/__tests__/exportContext.ts > maxDepth 1 exports the root thought and its direct children only

AssertionError: expected '- a\n - b\n - c\n - d\n - e' to be '- a\n - b\n - d' // Object.is equality - Expected + Received - a - b - - d + - c + - d + - e ❯ src/selectors/__tests__/exportContext.ts:272:20
- b
- d`)
})

it('maxDepth 0 exports only the root thought as HTML with no children', () => {
const text = `
- a
- b
`

const steps = [importText({ text }), setCursor(['a'])]
const stateNew = reducerFlow(steps)(initialState())
const exported = exportContext(stateNew, ['a'], 'text/html', { maxDepth: 0 })

expect(exported).toBe(`<ul>

Check failure on line 287 in src/selectors/__tests__/exportContext.ts

View workflow job for this annotation

GitHub Actions / TDD — Unit tests

[unit] src/selectors/__tests__/exportContext.ts > maxDepth 0 exports only the root thought as HTML with no children

AssertionError: expected '<ul>\n <li>a \n <ul>\n <li>…' to be '<ul>\n <li>a </li>\n</ul>' // Object.is equality - Expected + Received <ul> - <li>a </li> + <li>a + <ul> + <li>b</li> + </ul> + </li> </ul> ❯ src/selectors/__tests__/exportContext.ts:287:20
<li>a </li>
</ul>`)
})
14 changes: 12 additions & 2 deletions src/selectors/exportContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,21 @@ interface Options {
excludeMeta?: boolean
/** Exclude archived thoughts. */
excludeArchived?: boolean
/**
* Limits the export depth relative to the root thought.
* When set to 0, only the root thought is exported with no children.
* When set to 1, the root thought and its direct children are exported.
* When undefined, all descendants are exported (default).
*/
maxDepth?: number
}

/** Exports the navigable subtree of the given context. */
export const exportContext = (
state: State,
contextOrThoughtId: Context | ThoughtId,
format: MimeType = 'text/html',
{ indent = 0, title, excludeMarkdownFormatting, excludeMeta, excludeSrc, excludeArchived }: Options = {},
{ indent = 0, title, excludeMarkdownFormatting, excludeMeta, excludeSrc, excludeArchived, maxDepth }: Options = {},
): string => {
const linePostfix = format === 'text/html' ? (indent === 0 ? ' ' : '') + '</li>' : ''
const tab0 = Array(indent).fill('').join(' ')
Expand All @@ -61,7 +68,9 @@ export const exportContext = (
const context = Array.isArray(contextOrThoughtId) ? contextOrThoughtId : thoughtToContext(state, thoughtId!)
const isNoteAndMetaExcluded = excludeMeta && head(context) === '=note'

const childrenFiltered = children.filter(exportFilter({ excludeArchived, excludeMeta }))
const childrenFiltered = (maxDepth !== undefined && maxDepth <= 0 ? [] : children).filter(
exportFilter({ excludeArchived, excludeMeta }),
)

// Note: export single thought without bullet
const linePrefix = format === 'text/html' ? '<li>' : '- '
Expand All @@ -74,6 +83,7 @@ export const exportContext = (
excludeMeta,
excludeArchived,
excludeMarkdownFormatting,
maxDepth: maxDepth !== undefined ? maxDepth - 1 : undefined,
indent: indent + (isNoteAndMetaExcluded ? 0 : format === 'text/html' ? (indent === 0 ? 3 : 2) : 1),
})

Expand Down
Loading