mAi: #1 - schema: date_ranked_choice question type
New discriminated-union variant for `FeedbackQuestionSchema`:
{ type: 'date_ranked_choice',
options: [{ id, start, end?, label? }, …],
scale?: { min_label?, max_label? },
allow_partial?: boolean }
- Times stored as UTC ISO 8601 strings (datetime with offset). Author UI
feeds them through datetime-local inputs that the browser already treats
as local time; renderer converts back to viewer-local on display.
- Rating scale is locked at 1-5 (5-point Likert) per design — the `scale`
field exposes only labels, not min/max bounds.
- Per-option ids are 1-64 chars, alphanumeric + `-`/`_`, must be unique.
- 2-50 options per question.
Submission answer union extended with a `Record<string, 1|2|3|4|5|null>`
shape for the per-option rating map (`{ opt1: 5, opt2: null }`).
Refs m/fdbck#1.
This commit is contained in:
@@ -10,6 +10,16 @@ const FeedbackQuestionBaseSchema = z.object({
|
||||
help: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
/** One date/time option in a `date_ranked_choice` question. Times are stored as UTC ISO 8601 strings. */
|
||||
export const DateRankedOptionSchema = z.object({
|
||||
id: z.string().min(1).max(64).regex(/^[a-zA-Z0-9_-]+$/, {
|
||||
message: 'option id may only contain letters, digits, "-" and "_"',
|
||||
}),
|
||||
start: z.string().datetime({ offset: true }),
|
||||
end: z.string().datetime({ offset: true }).optional(),
|
||||
label: z.string().max(200).optional(),
|
||||
});
|
||||
|
||||
export const FeedbackQuestionSchema = z.discriminatedUnion('type', [
|
||||
FeedbackQuestionBaseSchema.extend({
|
||||
type: z.literal('short_text'),
|
||||
@@ -37,6 +47,20 @@ export const FeedbackQuestionSchema = z.discriminatedUnion('type', [
|
||||
FeedbackQuestionBaseSchema.extend({
|
||||
type: z.literal('boolean'),
|
||||
}),
|
||||
FeedbackQuestionBaseSchema.extend({
|
||||
type: z.literal('date_ranked_choice'),
|
||||
options: z.array(DateRankedOptionSchema).min(2).max(50)
|
||||
.refine(
|
||||
(opts) => new Set(opts.map((o) => o.id)).size === opts.length,
|
||||
{ message: 'date_ranked_choice option ids must be unique' },
|
||||
),
|
||||
// Scale is locked at 1-5 (5-point Likert) per design — only the labels are author-configurable.
|
||||
scale: z.object({
|
||||
min_label: z.string().max(50).optional(),
|
||||
max_label: z.string().max(50).optional(),
|
||||
}).optional(),
|
||||
allow_partial: z.boolean().optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Version stamp like `0.260505` (YYMMDD) or `0.260505.b` for same-day re-edits. */
|
||||
@@ -77,6 +101,14 @@ export const FeedbackInstanceUpdateSchema = z.object({
|
||||
|
||||
const ANSWER_MAX = 5000;
|
||||
|
||||
/** Per-option rating map for `date_ranked_choice` answers, e.g. `{ opt1: 5, opt2: null }`. */
|
||||
const DateRankedAnswerSchema = z.record(
|
||||
z.string().min(1).max(64),
|
||||
z.number().int().min(1).max(5).nullable(),
|
||||
).refine((m) => Object.keys(m).length <= 50, {
|
||||
message: 'too many rated options',
|
||||
});
|
||||
|
||||
export const FeedbackSubmissionSchema = z.object({
|
||||
display_name: z.string().max(80).nullable().optional()
|
||||
.transform((v) => (v && v.trim() ? v.trim().replace(/[\r\n]+/g, ' ') : null)),
|
||||
@@ -88,6 +120,7 @@ export const FeedbackSubmissionSchema = z.object({
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string().max(200)).max(20),
|
||||
DateRankedAnswerSchema,
|
||||
z.null(),
|
||||
]),
|
||||
),
|
||||
@@ -121,3 +154,5 @@ export const ShareCreateSchema = z.object({
|
||||
|
||||
export type FeedbackQuestion = z.infer<typeof FeedbackQuestionSchema>;
|
||||
export type FeedbackFormDefinition = z.infer<typeof FeedbackFormDefinitionSchema>;
|
||||
export type DateRankedOption = z.infer<typeof DateRankedOptionSchema>;
|
||||
export type DateRankedAnswer = z.infer<typeof DateRankedAnswerSchema>;
|
||||
|
||||
Reference in New Issue
Block a user