Skip to content
Merged
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
8 changes: 5 additions & 3 deletions src/frontend/fields/list/flat-list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import type { FieldSpec } from '../../../types';
import Icon from '../../shared/icon.vue';
import AddItemButton from '../../shared/add-item-button.vue';
import { useWidgetsStore, useSharedStore, useModelStore } from '../../store';
import { useListStateStore } from '../../store/list-state';

const props = defineProps({
field: {
Expand Down Expand Up @@ -135,6 +136,7 @@ const emit = defineEmits(['addSet', 'removeSet']);
const field = computed(() => props.field as FieldSpec);
const fields = field.value.fields as FieldSpec[];
const widgets = useWidgetsStore();
const listState = useListStateStore();
const shared = useSharedStore();
const model = useModelStore();

Expand Down Expand Up @@ -169,8 +171,8 @@ const hasSourceItem = (index: number): boolean => {

// This does soft remove
const toggleRemove = (index: number) => {
const isCurrentlyRemoved = widgets.isInRemovedList(props.fieldPath, index);
widgets.toggleRemovedIndex(props.fieldPath, index);
const isCurrentlyRemoved = listState.isInRemovedList(props.fieldPath, index);
listState.toggleRemovedIndex(props.fieldPath, index);

if (!isCurrentlyRemoved) {
const list = [...(model.getField(props.fieldPath, []) as Record<string, unknown>[])];
Expand All @@ -180,7 +182,7 @@ const toggleRemove = (index: number) => {
};

const isRemoved = (index: number): boolean => {
return widgets.isInRemovedList(props.fieldPath, index);
return listState.isInRemovedList(props.fieldPath, index);
};

const isSubgridWidget = (widget: string): boolean => {
Expand Down
16 changes: 9 additions & 7 deletions src/frontend/fields/list/foldable-list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import type { PropType } from 'vue';
import type { FieldSpec } from '../../../types';
import Icon from '../../shared/icon.vue';
import { useModelStore, useWidgetsStore, useSharedStore } from '../../store';
import { useListStateStore } from '../../store/list-state';
import AddItemButton from '../../shared/add-item-button.vue';

const props = defineProps({
Expand Down Expand Up @@ -168,6 +169,7 @@ const field = computed(() => props.field as FieldSpec);
const fields = field.value.fields as FieldSpec[];
const model = useModelStore();
const widgets = useWidgetsStore();
const listState = useListStateStore();
const shared = useSharedStore();

const canMutate = computed(() => {
Expand All @@ -181,14 +183,14 @@ const isIsland = (type: string): boolean => {
return singleWidgets.includes(type);
};

const toggleState = computed(() => widgets.getListToggles(props.fieldPath));
const toggleState = computed(() => listState.getListToggles(props.fieldPath));

const ensureToggles = () => {
const current = toggleState.value;
if (current.length < props.listItems.length) {
const needed = props.listItems.length - current.length;
const fresh = [...current, ...new Array(needed).fill(true)];
widgets.setListToggles(props.fieldPath, fresh);
listState.setListToggles(props.fieldPath, fresh);
}
};

Expand All @@ -202,12 +204,12 @@ const toggle = (index: number) => {
const fresh = toggleState.value;
fresh[index] = !fresh[index];

widgets.setListToggles(props.fieldPath, fresh);
listState.setListToggles(props.fieldPath, fresh);
};

const toggleRemove = (index: number) => {
const isCurrentlyRemoved = widgets.isInRemovedList(props.fieldPath, index);
widgets.toggleRemovedIndex(props.fieldPath, index);
const isCurrentlyRemoved = listState.isInRemovedList(props.fieldPath, index);
listState.toggleRemovedIndex(props.fieldPath, index);

if (!isCurrentlyRemoved) {
const list = [...(model.getField(props.fieldPath, []) as Record<string, unknown>[])];
Expand All @@ -217,14 +219,14 @@ const toggleRemove = (index: number) => {
const fresh = toggleState.value;
if (fresh[index]) {
fresh[index] = false;
widgets.setListToggles(props.fieldPath, fresh);
listState.setListToggles(props.fieldPath, fresh);
}
}
};

const isRemoved = (index: number): boolean => {
if (props.isReadOnly) return false;
return widgets.isInRemovedList(props.fieldPath, index);
return listState.isInRemovedList(props.fieldPath, index);
};

const hasSourceItem = (index: number): boolean => {
Expand Down
13 changes: 12 additions & 1 deletion src/frontend/fields/markdown-field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
</template>

<script setup lang="ts">
import { computed, ref, nextTick, onMounted } from 'vue';
import { computed, ref, nextTick, onMounted, watch } from 'vue';
import type { FieldSpec } from '../../types';
import { useModelStore, useSharedStore } from '../store';
import { commonProps, expandShortcuts } from '../shared/helpers';
Expand Down Expand Up @@ -94,6 +94,17 @@ const hasError = computed(() => errors.value.length > 0 && !props.isReadOnly);

let mde: EasyMDE | null = null;

watch(
() => shared.showSourceColumn,
async (isVisible) => {
if (!isVisible || !props.isReadOnly) return;

// CodeMirror cannot measure itself while its source column is hidden.
Comment thread
timosville marked this conversation as resolved.
await nextTick();
mde?.codemirror.refresh();
},
);

const applyEditorDirection = () => {
if (!mde) return;

Expand Down
44 changes: 44 additions & 0 deletions src/frontend/store/list-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useListStateStore = defineStore('list-state', () => {
const removedItems = ref<Record<string, number[]>>({});
const listToggles = ref<Record<string, boolean[]>>({});

const clearListState = (): void => {
removedItems.value = {};
listToggles.value = {};
};

const setListToggles = (path: string, value: boolean[]): void => {
const fresh = { ...listToggles.value };
fresh[path] = value;
listToggles.value = fresh;
};

const getListToggles = (path: string): boolean[] => listToggles.value[path] || [];

const toggleRemovedIndex = (path: string, index: number): void => {
const fresh = { ...removedItems.value };
fresh[path] = Array.from(removedItems.value[path] || []);
if (fresh[path].includes(index)) {
fresh[path].splice(fresh[path].indexOf(index), 1);
} else {
fresh[path].push(index);
}
removedItems.value = fresh;
};

const isInRemovedList = (path: string, index: number): boolean =>
removedItems.value[path]?.includes(index) || false;

return {
removedItems,
listToggles,
clearListState,
setListToggles,
getListToggles,
toggleRemovedIndex,
isInRemovedList,
};
});
5 changes: 5 additions & 0 deletions src/frontend/store/model.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { Scripture, SharedPageProps, DraftEditProps } from '../../types';
import { useListStateStore } from './list-state';

export const useModelStore = defineStore('model', () => {
const model = ref({});
const listState = useListStateStore();

const resolvePath = (
object: Record<string | number, any>,
Expand Down Expand Up @@ -48,6 +50,7 @@ export const useModelStore = defineStore('model', () => {
resolvePath(model.value, path, defaultValue);

const setModel = (fresh: object) => {
listState.clearListState();
model.value = fresh;
};

Expand Down Expand Up @@ -79,6 +82,8 @@ export const useModelStore = defineStore('model', () => {
};

const setFromProps = (props: DraftEditProps & SharedPageProps) => {
listState.clearListState();

if (props.bundle) {
model.value = { ...props.bundle };
}
Expand Down
32 changes: 0 additions & 32 deletions src/frontend/store/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,6 @@ export const useWidgetsStore = defineStore('widgets', () => {
picker.value = fresh;
};

// track folding in foldable lists

const listToggles = ref<Record<string, boolean[]>>({});
const setListToggles = (path: string, value: boolean[]): void => {
const fresh = { ...listToggles.value };
fresh[path] = value;
listToggles.value = fresh;
};
const getListToggles = (path: string): boolean[] => listToggles.value[path] || [];

// track removed items in flexible lists

const removedItems = ref<Record<string, number[]>>({});
const toggleRemovedIndex = (path: string, index: number): void => {
const fresh = { ...removedItems.value };
fresh[path] = Array.from(removedItems.value[path] || []);
if (fresh[path].includes(index)) {
fresh[path].splice(fresh[path].indexOf(index), 1);
} else {
fresh[path].push(index);
}
removedItems.value = fresh;
};
const isInRemovedList = (path: string, index: number): boolean =>
removedItems.value[path]?.includes(index) || false;

// providers

const providers = ref<Providers>(defaultProviders);
Expand All @@ -88,12 +62,6 @@ export const useWidgetsStore = defineStore('widgets', () => {
setPicker,
standardPicker,

getListToggles,
setListToggles,

toggleRemovedIndex,
isInRemovedList,

setProviders,
providers,

Expand Down
29 changes: 29 additions & 0 deletions tests/e2e/translationIndex.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
await page.goto('/src-draft-translationindex-story-vue');
await page.getByRole('link', { name: 'Translation Page 6' }).click();
await page.getByRole('link', { name: 'Index', exact: true }).click();
});

test.afterEach(async ({ page }) => {
await page.close();
});

test('shows source markdown immediately after reopening the source column', async ({
page,
}) => {
const preview = page.frameLocator('[data-test-id="preview-iframe"]');
const sourceToggle = preview.getByRole('button', { name: 'English' });
const sourceMarkdown = preview.getByText('## The Word Became Flesh', {
exact: false,
});

await expect(sourceMarkdown).toBeVisible();

await sourceToggle.click();
await expect(sourceMarkdown).toBeHidden();

await sourceToggle.click();
await expect(sourceMarkdown).toBeVisible();
});
48 changes: 48 additions & 0 deletions tests/unit/listState.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { test, expect } from '@playwright/test';
import { createPinia, setActivePinia } from 'pinia';
import { useListStateStore } from '../../src/frontend/store/list-state';

test.describe('List state', () => {
test.beforeEach(async () => {
setActivePinia(createPinia());
});

test('toggleRemovedIndex adds and removes indices', () => {
const store = useListStateStore();

store.toggleRemovedIndex('questions', 1);
expect(store.isInRemovedList('questions', 1)).toBe(true);
expect(store.isInRemovedList('questions', 0)).toBe(false);

store.toggleRemovedIndex('questions', 1);
expect(store.isInRemovedList('questions', 1)).toBe(false);
});

test('clearListState clears removedItems and listToggles', () => {
const store = useListStateStore();

store.toggleRemovedIndex('questions', 1);
store.setListToggles('resources', [true, false]);

store.clearListState();

expect(store.isInRemovedList('questions', 1)).toBe(false);
expect(store.getListToggles('resources')).toEqual([]);
});

test('list state is isolated between Pinia instances', () => {
const first = useListStateStore(createPinia());
const second = useListStateStore(createPinia());

first.toggleRemovedIndex('questions', 1);
first.setListToggles('resources', [true, false]);

expect(second.isInRemovedList('questions', 1)).toBe(false);
expect(second.getListToggles('resources')).toEqual([]);

second.clearListState();

expect(first.isInRemovedList('questions', 1)).toBe(true);
expect(first.getListToggles('resources')).toEqual([true, false]);
});
});
Loading
Loading