Skip to content

Commit d75101f

Browse files
BC-10854 - collaboration fix elements (#3954)
Fix problems when doing collaboration on boards: - Fix internal state handling of (board-, column-, card-) titles - Fix internal state handling of richtext-elements - Fix updating of external-tools-element - updateing the element when tool was configured - Fix publishing of duplicateCardSuccess-message
1 parent b8b2916 commit d75101f

File tree

8 files changed

+112
-115
lines changed

8 files changed

+112
-115
lines changed

src/modules/data/board/Card.store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ export const useCardStore = defineStore("cardStore", () => {
7979
const duplicateCardSuccess = (payload: DuplicateCardSuccessPayload) => {
8080
if (payload.duplicatedCard.id) {
8181
cards.value[payload.duplicatedCard.id] = payload.duplicatedCard;
82-
notifyInfo("components.board.notifications.info.cardDuplicated");
82+
if (payload.isOwnAction === true) {
83+
notifyInfo("components.board.notifications.info.cardDuplicated");
84+
}
8385
}
8486
};
8587

src/modules/feature/board-external-tool-element/ExternalToolElement.vue

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ import { mdiPuzzleOutline } from "@icons/material";
7979
import { ContentElementBar } from "@ui-board";
8080
import { LineClamp } from "@ui-line-clamp";
8181
import { useSharedLastCreatedElement } from "@util-board";
82-
import { computed, ComputedRef, onMounted, onUnmounted, PropType, Ref, ref, toRef } from "vue";
82+
import { computed, ComputedRef, onMounted, onUnmounted, PropType, Ref, ref, toRef, watch } from "vue";
8383
import { useI18n } from "vue-i18n";
8484
8585
const props = defineProps({
@@ -128,7 +128,9 @@ const getIcon: ComputedRef<string | undefined> = computed(() => {
128128
129129
const { lastCreatedElementId, resetLastCreatedElementId } = useSharedLastCreatedElement();
130130
131-
const hasLinkedTool: ComputedRef<boolean> = computed(() => !!modelValue.value.contextExternalToolId);
131+
const hasLinkedTool: ComputedRef<boolean> = computed(
132+
() => modelValue.value.contextExternalToolId !== null || props.element.content.contextExternalToolId !== null
133+
);
132134
133135
const isDeepLinkingTool: ComputedRef<boolean> = computed(() => !!displayData.value?.isLtiDeepLinkingTool);
134136
@@ -192,19 +194,13 @@ const onKeydownArrow = (event: KeyboardEvent) => {
192194
}
193195
};
194196
195-
const onMoveElementDown = () => {
196-
emit("move-down:edit");
197-
};
197+
const onMoveElementDown = () => emit("move-down:edit");
198198
199-
const onMoveElementUp = () => {
200-
emit("move-up:edit");
201-
};
199+
const onMoveElementUp = () => emit("move-up:edit");
202200
203201
const onDeleteElement = () => emit("delete:element", element.value.id);
204202
205-
const onEditElement = () => {
206-
isConfigurationDialogOpen.value = true;
207-
};
203+
const onEditElement = () => (isConfigurationDialogOpen.value = true);
208204
209205
const onClickElement = async () => {
210206
if (hasLinkedTool.value && (!props.isEditMode || (props.isEditMode && isDeepLinkingTool.value))) {
@@ -220,9 +216,7 @@ const onClickElement = async () => {
220216
}
221217
};
222218
223-
const onConfigurationDialogClose = () => {
224-
isConfigurationDialogOpen.value = false;
225-
};
219+
const onConfigurationDialogClose = () => (isConfigurationDialogOpen.value = false);
226220
227221
const onConfigurationDialogSave = async (tool: ContextExternalTool) => {
228222
modelValue.value.contextExternalToolId = tool.id;
@@ -231,11 +225,11 @@ const onConfigurationDialogSave = async (tool: ContextExternalTool) => {
231225
};
232226
233227
const loadCardData = async () => {
234-
if (modelValue.value.contextExternalToolId) {
235-
await fetchDisplayData(modelValue.value.contextExternalToolId);
228+
if (element.value.content.contextExternalToolId) {
229+
await fetchDisplayData(element.value.content.contextExternalToolId);
236230
237231
if (isToolLaunchable.value) {
238-
await fetchContextLaunchRequest(modelValue.value.contextExternalToolId);
232+
await fetchContextLaunchRequest(element.value.content.contextExternalToolId);
239233
}
240234
}
241235
};
@@ -258,6 +252,16 @@ onUnmounted(() => {
258252
clearInterval(timer);
259253
});
260254
255+
watch(
256+
() => element.value.content.contextExternalToolId,
257+
async () => {
258+
if (element.value.content.contextExternalToolId !== null) {
259+
modelValue.value.contextExternalToolId = element.value.content.contextExternalToolId;
260+
await loadCardData();
261+
}
262+
}
263+
);
264+
261265
const ariaLabel = computed(() => {
262266
const elementName = t("components.cardElement.externalToolElement");
263267
const information = [elementName];

src/modules/feature/board-text-element/RichTextContentElement.unit.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ const mockElement: RichTextElementResponse = {
2222

2323
vi.mock("@data-board", () => ({
2424
useBoardFocusHandler: vi.fn(),
25-
useContentElementState: vi.fn().mockImplementation(() => ({
26-
modelValue: mockElement.content,
27-
})),
2825
useDeleteConfirmationDialog: vi.fn(),
2926
}));
3027

src/modules/feature/board-text-element/RichTextContentElement.vue

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
<RichTextContentElementEdit
99
v-if="isEditMode"
1010
:autofocus="autofocus"
11-
:value="modelValue.text"
12-
:data-testid="`rich-text-edit-${columnIndex}-${elementIndex}`"
1311
:column-index="columnIndex"
14-
@update:value="onUpdateElement"
15-
@delete:element="onDeleteElement"
12+
:data-testid="`rich-text-edit-${columnIndex}-${elementIndex}`"
13+
:element="element"
14+
:is-edit-mode="isEditMode"
1615
@blur="onBlur"
16+
@delete:element="onDeleteElement"
1717
@keyup.capture="onKeyUp"
1818
/>
1919
</div>
@@ -24,7 +24,7 @@ import RichTextContentElementDisplay from "./RichTextContentElementDisplay.vue";
2424
import RichTextContentElementEdit from "./RichTextContentElementEdit.vue";
2525
import { useAriaLiveNotifier } from "@/composables/ariaLiveNotifier";
2626
import { RichTextElementResponse } from "@/serverApi/v3";
27-
import { useBoardFocusHandler, useContentElementState } from "@data-board";
27+
import { useBoardFocusHandler } from "@data-board";
2828
import { computed, PropType, ref, toRef } from "vue";
2929
3030
const props = defineProps({
@@ -39,26 +39,18 @@ const props = defineProps({
3939
4040
const emit = defineEmits(["delete:element"]);
4141
42-
const { modelValue } = useContentElementState(props);
4342
const { ensurePoliteNotifications } = useAriaLiveNotifier();
4443
45-
const autofocus = ref(false);
4644
const element = toRef(props, "element");
45+
46+
const autofocus = ref(false);
4747
useBoardFocusHandler(element.value.id, ref(null), () => {
4848
autofocus.value = true;
4949
});
5050
51-
const onDeleteElement = () => {
52-
emit("delete:element", element.value.id);
53-
};
54-
55-
const onUpdateElement = (text: string) => {
56-
modelValue.value.text = text;
57-
};
51+
const onBlur = () => (autofocus.value = false);
5852
59-
const onBlur = () => {
60-
autofocus.value = false;
61-
};
53+
const onDeleteElement = () => emit("delete:element", element.value.id);
6254
6355
const onKeyUp = () => ensurePoliteNotifications();
6456

src/modules/feature/board-text-element/RichTextContentElementEdit.unit.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
import RichTextContentElementEdit from "./RichTextContentElementEdit.vue";
2+
import { richTextElementResponseFactory } from "@@/tests/test-utils";
23
import { createTestingI18n, createTestingVuetify } from "@@/tests/test-utils/setup";
4+
import { useContentElementState } from "@data-board";
5+
import { createMock, DeepMocked } from "@golevelup/ts-vitest";
6+
import { createTestingPinia } from "@pinia/testing";
37
import { BOARD_IS_LIST_LAYOUT } from "@util-board";
4-
import { mount } from "@vue/test-utils";
5-
import { nextTick } from "vue";
8+
import { flushPromises, mount } from "@vue/test-utils";
9+
import { setActivePinia } from "pinia";
10+
import { ref } from "vue";
11+
12+
vi.mock("@data-board/ContentElementState.composable");
613

714
describe("RichTextContentElementEdit", () => {
15+
let useContentElementStateMock: DeepMocked<ReturnType<typeof useContentElementState>>;
16+
17+
beforeEach(() => {
18+
setActivePinia(createTestingPinia());
19+
useContentElementStateMock = createMock<ReturnType<typeof useContentElementState>>();
20+
vi.mocked(useContentElementState).mockReturnValue(useContentElementStateMock);
21+
});
22+
823
const setup = ({ autofocus = true }: { autofocus: boolean }) => {
24+
const element = richTextElementResponseFactory.build();
25+
const modelValueOfComposable = ref(element.content);
26+
useContentElementStateMock.modelValue = modelValueOfComposable;
27+
928
const wrapper = mount(RichTextContentElementEdit, {
1029
global: {
1130
plugins: [createTestingVuetify(), createTestingI18n()],
@@ -18,13 +37,14 @@ describe("RichTextContentElementEdit", () => {
1837
},
1938
},
2039
props: {
21-
value: "test value",
40+
element,
41+
isEditMode: true,
2242
autofocus,
2343
columnIndex: 0,
2444
},
2545
});
2646

27-
return { wrapper };
47+
return { wrapper, modelValueOfComposable };
2848
};
2949

3050
describe("when component is mounted", () => {
@@ -50,13 +70,14 @@ describe("RichTextContentElementEdit", () => {
5070
});
5171

5272
it("should update modalValue when prop value changes", async () => {
53-
const { wrapper } = setup({ autofocus: true });
73+
const { wrapper, modelValueOfComposable } = setup({ autofocus: true });
5474
const newValue = "new title";
55-
await wrapper.setProps({ value: newValue });
56-
await nextTick();
5775

58-
const emitted = wrapper.emitted();
59-
expect(emitted["update:value"]).toHaveLength(1);
76+
const inlineEditor = wrapper.getComponent({ name: "InlineEditor" });
77+
await inlineEditor.vm.$emit("update:value", newValue);
78+
await flushPromises();
79+
80+
expect(modelValueOfComposable.value.text).toBe(newValue);
6081
});
6182
});
6283
});
Lines changed: 42 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,68 @@
11
<template>
22
<InlineEditor
3-
v-model="modelValue"
3+
v-model="modelValue.text"
4+
class="cursor-text"
45
:autofocus="autofocus"
56
:placeholder="$t('components.cardElement.richTextElement.placeholder')"
6-
class="cursor-text"
77
:viewport-offset-top="offsetTop"
8-
@update:value="onUpdateValue"
9-
@focus="onFocus"
108
@blur="onBlur"
9+
@focus="onFocus"
1110
@keyboard:delete="onDelete"
11+
@update:value="onUpdateValue"
1212
/>
1313
</template>
1414

15-
<script lang="ts">
15+
<script setup lang="ts">
16+
import { RichTextElementResponse } from "@/serverApi/v3";
1617
import { injectStrict } from "@/utils/inject";
18+
import { useContentElementState } from "@data-board";
1719
import { InlineEditor } from "@feature-editor";
1820
import { useViewportOffsetTop } from "@ui-layout";
1921
import { BOARD_IS_LIST_LAYOUT } from "@util-board";
2022
import { useEventListener } from "@vueuse/core";
21-
import { computed, defineComponent, onMounted, ref, watch } from "vue";
23+
import { computed, PropType } from "vue";
2224
23-
export default defineComponent({
24-
name: "RichTextContentElementEdit",
25-
components: { InlineEditor },
26-
props: {
27-
value: {
28-
type: String,
29-
required: true,
30-
},
31-
autofocus: {
32-
type: Boolean,
33-
required: true,
34-
},
35-
columnIndex: {
36-
type: Number,
37-
required: true,
38-
},
25+
const props = defineProps({
26+
autofocus: {
27+
type: Boolean,
28+
required: true,
3929
},
40-
emits: ["update:value", "delete:element", "blur"],
41-
setup(props, { emit }) {
42-
const modelValue = ref("");
43-
44-
const isListLayout = injectStrict(BOARD_IS_LIST_LAYOUT);
45-
const offsetTop = computed(() => useViewportOffsetTop(props.columnIndex, isListLayout).offsetTop.value);
30+
columnIndex: {
31+
type: Number,
32+
required: true,
33+
},
34+
element: {
35+
type: Object as PropType<RichTextElementResponse>,
36+
required: true,
37+
},
38+
isEditMode: { type: Boolean, required: true },
39+
});
4640
47-
onMounted(() => {
48-
if (props.value !== undefined) {
49-
modelValue.value = props.value;
50-
}
51-
});
41+
const emit = defineEmits<{
42+
(e: "delete:element"): void;
43+
(e: "blur"): void;
44+
}>();
5245
53-
watch(modelValue, (newValue) => {
54-
if (newValue !== props.value) {
55-
emit("update:value", newValue);
56-
}
57-
});
46+
const { modelValue } = useContentElementState(props);
5847
59-
const onUpdateValue = (newValue: string) => (modelValue.value = newValue);
48+
const isListLayout = injectStrict(BOARD_IS_LIST_LAYOUT);
49+
const offsetTop = computed(() => useViewportOffsetTop(props.columnIndex, isListLayout).offsetTop.value);
6050
61-
const onFocus = () => {
62-
const ckBalloonPanelElements = document.getElementsByClassName("ck-balloon-panel");
51+
const onBlur = () => emit("blur");
6352
64-
for (const element of ckBalloonPanelElements) {
65-
useEventListener(element, "click", (event: PointerEvent) => {
66-
event.stopPropagation();
67-
});
68-
}
69-
};
53+
const onDelete = () => emit("delete:element");
7054
71-
const onBlur = () => {
72-
emit("update:value", modelValue.value);
73-
emit("blur");
74-
};
55+
const onFocus = () => {
56+
const ckBalloonPanelElements = document.getElementsByClassName("ck-balloon-panel");
7557
76-
const onDelete = () => emit("delete:element");
58+
for (const element of ckBalloonPanelElements) {
59+
useEventListener(element, "click", (event: PointerEvent) => {
60+
event.stopPropagation();
61+
});
62+
}
63+
};
7764
78-
return {
79-
modelValue,
80-
offsetTop,
81-
onFocus,
82-
onDelete,
83-
onBlur,
84-
onUpdateValue,
85-
};
86-
},
87-
});
65+
const onUpdateValue = (newValue: string) => {
66+
modelValue.value.text = newValue;
67+
};
8868
</script>

src/modules/feature/board/shared/BoardAnyTitleInput.unit.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ describe("BoardAnyTitleInput", () => {
145145
await textFieldComponent.setValue(newTitle);
146146

147147
await wrapper.setProps({ isEditMode: false });
148+
await wrapper.setProps({ value: newTitle }); // simulate prop update after edit mode
148149
const heading = wrapper.find("h1");
149150

150151
expect(heading.text()).toBe(newTitle);
@@ -162,6 +163,7 @@ describe("BoardAnyTitleInput", () => {
162163
await textFieldComponent.setValue("");
163164

164165
await wrapper.setProps({ isEditMode: false });
166+
await wrapper.setProps({ value: "" }); // simulate prop update after edit mode
165167
const heading = wrapper.find("h1");
166168

167169
expect(heading.text()).toBe(emptyValueFallback);

0 commit comments

Comments
 (0)