Skip to content

Commit 30975cb

Browse files
committed
pass loading state to ui
1 parent b02000a commit 30975cb

6 files changed

Lines changed: 64 additions & 17 deletions

File tree

packages/web-forms/src/components/form-elements/upload/UploadControl.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const fileName = computed(() => props.question.currentState.value?.name ?? '');
3636
const accept = computed(() => props.question.nodeOptions.media.accept);
3737
const mediaType = computed(() => props.question.nodeOptions.media.type);
3838
const maxFileSize = computed(() => formOptions?.attachmentMaxSize ?? MAX_FILE_SIZE);
39+
const loading = computed(() => props.question.currentState.attachmentState.loading);
40+
const existingFileName = computed(() => props.question.currentState.attachmentState.intrinsicName ?? '');
3941
const confirmDeleteAction = ref(false);
4042
const fileError = ref<string | null>(null);
4143
@@ -168,7 +170,10 @@ const onDrop = (event: DragEvent) => {
168170
</template>
169171
<template #default>
170172
<div class="drag-and-drop" :class="{ 'disabled': isDisabled }" @drop.prevent.stop="onDrop" @dragover.prevent>
171-
<div v-if="question.currentState.value" class="upload-content">
173+
<div v-if="loading" class="skeleton-loading">
174+
{{ existingFileName }}
175+
</div>
176+
<div v-else-if="question.currentState.value" class="upload-content">
172177
<template v-if="fileType === 'image'">
173178
<UploadImagePreview :image="objectURL" />
174179
</template>
@@ -260,6 +265,14 @@ const onDrop = (event: DragEvent) => {
260265
.drag-and-drop {
261266
padding: var(--odk-spacing-xxl);
262267
268+
.skeleton-loading {
269+
display: flex;
270+
justify-content: center;
271+
align-items: center;
272+
width: 300px;
273+
height: 100px;
274+
}
275+
263276
&.disabled {
264277
color: var(--odk-muted-text-color);
265278
}

packages/xforms-engine/src/client/UploadNode.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { PartiallyKnownString } from '@getodk/common/types/string/PartiallyKnownString.ts';
2+
import type { BaseInstanceAttachmentState } from '../lib/reactivity/createInstanceAttachment.ts';
23
import type { UnknownAppearanceDefinition } from '../parse/body/appearance/unknownAppearanceParser.ts';
34
import type { UploadControlDefinition } from '../parse/body/control/UploadControlDefinition.ts';
45
import type { LeafNodeDefinition } from '../parse/model/LeafNodeDefinition.ts';
@@ -15,6 +16,7 @@ export interface UploadNodeState extends BaseValueNodeState<UploadValue> {
1516
get valueOptions(): null;
1617
get value(): UploadValue;
1718
get instanceValue(): InstanceAttachmentFileName;
19+
get attachmentState(): BaseInstanceAttachmentState;
1820
}
1921

2022
export interface UploadDefinition<V extends ValueType = ValueType> extends LeafNodeDefinition<V> {

packages/xforms-engine/src/instance/UploadControl.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
createAttributeState,
1414
type AttributeState,
1515
} from '../lib/reactivity/createAttributeState.ts';
16-
import { createInstanceAttachment } from '../lib/reactivity/createInstanceAttachment.ts';
16+
import {
17+
createInstanceAttachment,
18+
type BaseInstanceAttachmentState,
19+
} from '../lib/reactivity/createInstanceAttachment.ts';
1720
import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
1821
import type { EngineState } from '../lib/reactivity/node-state/createEngineState.ts';
1922
import type { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
@@ -66,7 +69,8 @@ interface UploadControlStateSpec extends DescendantNodeStateSpec<InstanceAttachm
6669
readonly attributes: Accessor<readonly Attribute[]>;
6770
readonly valueOptions: null;
6871
readonly value: SimpleAtomicState<InstanceAttachmentRuntimeValue>;
69-
readonly instanceValue: Accessor<InstanceAttachmentFileName>;
72+
readonly instanceValue: Accessor<InstanceAttachmentFileName>; // TODO merge this with state below?
73+
readonly attachmentState: Accessor<BaseInstanceAttachmentState>;
7074
}
7175

7276
export class UploadControl
@@ -143,6 +147,7 @@ export class UploadControl
143147
const instanceAttachment = createInstanceAttachment(this);
144148

145149
this.instanceAttachment = instanceAttachment;
150+
146151
this.attributeState = createAttributeState(this.scope);
147152
this.decodeInstanceValue = instanceAttachment.decodeInstanceValue;
148153
this.getXPathValue = instanceAttachment.getInstanceValue;
@@ -162,6 +167,7 @@ export class UploadControl
162167
value: instanceAttachment.valueState,
163168
attributes: this.attributeState.getAttributes,
164169
instanceValue: instanceAttachment.getInstanceValue,
170+
attachmentState: instanceAttachment.getState,
165171
},
166172
this.instanceConfig
167173
);

packages/xforms-engine/src/instance/attachments/InstanceAttachment.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Accessor } from 'solid-js';
22
import type { InstancePayload } from '../../client/index.ts';
3+
import type { BaseInstanceAttachmentState } from '../../lib/reactivity/createInstanceAttachment.ts';
34
import type { SimpleAtomicState, SimpleAtomicStateSetter } from '../../lib/reactivity/types.ts';
45
import type { InstanceAttachmentContext } from '../internal-api/InstanceAttachmentContext.ts';
56
import type { DecodeInstanceValue } from '../internal-api/InstanceValueContext.ts';
@@ -16,6 +17,8 @@ export interface InstanceAttachmentOptions {
1617
readonly getValue: Accessor<InstanceAttachmentRuntimeValue>;
1718
readonly setValue: SimpleAtomicStateSetter<InstanceAttachmentRuntimeValue>;
1819
readonly valueState: SimpleAtomicState<InstanceAttachmentRuntimeValue>;
20+
21+
readonly getState: Accessor<BaseInstanceAttachmentState>;
1922
}
2023

2124
export class InstanceAttachment {
@@ -58,12 +61,15 @@ export class InstanceAttachment {
5861
readonly setValue: SimpleAtomicStateSetter<InstanceAttachmentRuntimeValue>;
5962
readonly valueState: SimpleAtomicState<InstanceAttachmentRuntimeValue>;
6063

64+
readonly getState: Accessor<BaseInstanceAttachmentState>;
65+
6166
private constructor(options: InstanceAttachmentOptions) {
6267
this.getFileName = options.getFileName;
6368
this.getInstanceValue = options.getInstanceValue;
6469
this.decodeInstanceValue = options.decodeInstanceValue;
6570
this.getValue = options.getValue;
6671
this.setValue = options.setValue;
6772
this.valueState = options.valueState;
73+
this.getState = options.getState;
6874
}
6975
}

packages/xforms-engine/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import { ErrorProductionDesignPendingError } from '../../../error/ErrorProductio
1515
import type { InstanceAttachmentsState } from '../../../instance/attachments/InstanceAttachmentsState.ts';
1616
import type { ClientReactiveSerializableInstance } from '../../../instance/internal-api/serialization/ClientReactiveSerializableInstance.ts';
1717

18+
// TODO only collect files that have changed (based on filename) otherwise we're uploading files unnecessarily
19+
// TODO make sure this ^ works when async files haven't yet loaded or loading has failed
20+
// TODO this will also impact validation - required files that have not yet loaded are valid!
1821
const collectInstanceAttachmentFiles = (attachments: InstanceAttachmentsState): readonly File[] => {
1922
const files = Array.from(attachments.entries()).map(([context, attachment]) => {
2023
if (!context.isAttached() || !context.isRelevant()) {

packages/xforms-engine/src/lib/reactivity/createInstanceAttachment.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,20 @@ interface InstanceAttachmentValueOptions {
6464
readonly nodeId: FormNodeID;
6565
readonly writtenAt: Date | null;
6666
readonly file: InstanceAttachmentRuntimeValue;
67+
readonly loading: boolean;
68+
readonly existingName?: string | null;
6769
}
6870

69-
interface BaseInstanceAttachmentState {
71+
export interface BaseInstanceAttachmentState {
7072
readonly computedName: string | null;
7173
readonly intrinsicName: string | null;
7274
readonly file: InstanceAttachmentRuntimeValue;
75+
readonly loading: boolean;
7376
}
7477

7578
interface BlankInstanceAttachmentState extends BaseInstanceAttachmentState {
7679
readonly computedName: null;
77-
readonly intrinsicName: null;
80+
readonly intrinsicName: string | null;
7881
readonly file: null;
7982
}
8083

@@ -93,25 +96,26 @@ const instanceAttachmentState = (
9396
context: InstanceAttachmentContext,
9497
options: InstanceAttachmentValueOptions
9598
): InstanceAttachmentState => {
96-
const { nodeId, file, writtenAt } = options;
99+
const { nodeId, file, writtenAt, loading, existingName } = options;
97100

98101
// No file -> no intrinsic name, no name to compute
99102
if (file == null) {
100103
return {
101104
computedName: null,
102-
intrinsicName: null,
105+
intrinsicName: existingName ?? null,
103106
file: null,
107+
loading,
104108
};
105109
}
106110

107-
const intrinsicName = file.name;
108-
109111
// File exists, not written by client -> preserve instance input name
112+
const intrinsicName = file.name;
110113
if (writtenAt == null) {
111114
return {
112115
computedName: null,
113116
intrinsicName,
114117
file,
118+
loading,
115119
};
116120
}
117121

@@ -128,6 +132,7 @@ const instanceAttachmentState = (
128132
computedName,
129133
intrinsicName,
130134
file,
135+
loading,
131136
};
132137
};
133138

@@ -139,23 +144,32 @@ export const createInstanceAttachment = (
139144
const { attachments } = rootDocument;
140145

141146
const filePromise = attachments.getInitialFileValue(context.instanceNode);
147+
const existingName = context.instanceNode?.value ?? null;
142148
const initialState = instanceAttachmentState(context, {
143149
nodeId,
144-
file: null, // TODO add a loading concept to distinguish from empty value
150+
file: null,
145151
writtenAt: null,
152+
loading: !!filePromise,
153+
existingName,
146154
});
147155

148156
const [getState, setState] = createSignal<InstanceAttachmentState>(initialState);
149157

150158
if (filePromise) {
151-
void Promise.resolve(filePromise).then((file: File) => {
152-
const resolvedState = instanceAttachmentState(context, {
153-
nodeId,
154-
file,
155-
writtenAt: null,
159+
void Promise.resolve(filePromise)
160+
.then((file: File) => {
161+
const resolvedState = instanceAttachmentState(context, {
162+
nodeId,
163+
file,
164+
writtenAt: null,
165+
loading: false,
166+
existingName,
167+
});
168+
setState(resolvedState);
169+
})
170+
.catch((_) => {
171+
// TODO set error state
156172
});
157-
setState(resolvedState);
158-
});
159173
}
160174

161175
const decodeInstanceValue: DecodeInstanceValue = (value) => {
@@ -198,6 +212,7 @@ export const createInstanceAttachment = (
198212
nodeId,
199213
file: value,
200214
writtenAt: new Date(),
215+
loading: false,
201216
});
202217

203218
return setState(updatedState).file;
@@ -219,6 +234,8 @@ export const createInstanceAttachment = (
219234
getValue,
220235
setValue,
221236
valueState,
237+
238+
getState,
222239
});
223240
});
224241
};

0 commit comments

Comments
 (0)