|
2 | 2 |
|
3 | 3 | import { describe, it, expect } from 'vitest'; |
4 | 4 | import { mergeViewsIntoObjects } from './MetadataProvider'; |
| 5 | +import { resolveFormViewLayout } from '../utils/recordFormNavigation'; |
5 | 6 |
|
6 | 7 | /** |
7 | 8 | * `mergeViewsIntoObjects` folds the `view` metadata type into object |
@@ -147,6 +148,137 @@ describe('mergeViewsIntoObjects', () => { |
147 | 148 | // …and the list family is untouched by the form entry. |
148 | 149 | expect(Object.keys(obj.listViews)).toEqual(['crm_activity.default']); |
149 | 150 | }); |
| 151 | + |
| 152 | + it('takes a container\'s declared `form` as `.form`, never a named form beside it', () => { |
| 153 | + // The default form is expanded AFTER the named ones, so arrival order |
| 154 | + // cannot be what picks it. |
| 155 | + const container = { |
| 156 | + name: 'crm_activity', |
| 157 | + list: { type: 'grid', columns: [{ field: 'subject' }] }, |
| 158 | + form: { type: 'simple', label: 'Activity', sections: [{ name: 'details', label: 'Details', fields: [{ field: 'subject' }] }] }, |
| 159 | + formViews: { quick: { type: 'simple', label: 'Quick log', sections: [{ name: 'quick', label: 'Quick', fields: [{ field: 'subject' }] }] } }, |
| 160 | + }; |
| 161 | + const [obj] = mergeViewsIntoObjects(objects, [container]); |
| 162 | + expect(obj.form.name).toBe('crm_activity.form'); |
| 163 | + expect(obj.form.sections.map((s: any) => s.name)).toEqual(['details']); |
| 164 | + expect(obj.formViews['crm_activity.quick'].isDefault).toBe(false); |
| 165 | + }); |
| 166 | + }); |
| 167 | +}); |
| 168 | + |
| 169 | +/** |
| 170 | + * `.form` is the served DEFAULT form only (objectui#11539). |
| 171 | + * |
| 172 | + * `@objectstack/spec` `ViewSchema`: `form` is a container's "Default form |
| 173 | + * view", `formViews` are "Additional named form views". The served rows carry |
| 174 | + * that: the container's own `form` arrives as `OBJECT.form` with `isDefault`, |
| 175 | + * and a container with no `form` serves its named forms with no `isDefault` |
| 176 | + * key at all (objectstack#21500). The console used to make the FIRST form item |
| 177 | + * to arrive its create/edit `.form` whenever none was default, so a named-only |
| 178 | + * container rendered its first named form — in the `showcase_inquiry` case the |
| 179 | + * public, anonymous contact-us form — as the create and edit form. |
| 180 | + * |
| 181 | + * Fixtures are served `view` rows in the shape the expansion emits |
| 182 | + * (`{ name, object, viewKind, label, config, order, scope, isDefault? }`), in |
| 183 | + * the order it emits them: named entries first, the container's own default |
| 184 | + * after them. |
| 185 | + */ |
| 186 | +describe('mergeViewsIntoObjects — `.form` is the served default form only (objectui#11539)', () => { |
| 187 | + const row = (object: string, key: string, viewKind: 'list' | 'form', config: Record<string, any>, extra: Record<string, any> = {}) => ({ |
| 188 | + name: `${object}.${key}`, |
| 189 | + object, |
| 190 | + viewKind, |
| 191 | + label: config.label, |
| 192 | + config, |
| 193 | + scope: 'package', |
| 194 | + ...extra, |
| 195 | + }); |
| 196 | + |
| 197 | + // examples/app-showcase `showcase_inquiry`: a default list, a named list, and |
| 198 | + // ONE named form — the public contact-us form. No `form`, so no form row is |
| 199 | + // default. |
| 200 | + const inquiryData = { provider: 'object', object: 'showcase_inquiry' }; |
| 201 | + const inquiryRows = [ |
| 202 | + row('showcase_inquiry', 'triage', 'list', { type: 'grid', label: 'Inquiry Triage', data: inquiryData, columns: [{ field: 'name' }] }, { order: 0 }), |
| 203 | + row('showcase_inquiry', 'default', 'list', { type: 'grid', label: 'Inquiries', data: inquiryData, columns: [{ field: 'name' }] }, { order: 1, isDefault: true }), |
| 204 | + row('showcase_inquiry', 'contact', 'form', { |
| 205 | + type: 'simple', |
| 206 | + data: inquiryData, |
| 207 | + sections: [{ name: 'tell_us_about_yourself', label: 'Tell us about yourself', fields: [{ field: 'name' }, { field: 'email' }] }], |
| 208 | + sharing: { enabled: true, allowAnonymous: true, publicLink: '/forms/contact-us' }, |
| 209 | + }, { order: 2 }), |
| 210 | + ]; |
| 211 | + |
| 212 | + // examples/app-crm `crm_lead`: the named Web-to-Lead form, then the |
| 213 | + // container's own `form`, the one default. |
| 214 | + const leadData = { provider: 'object', object: 'crm_lead' }; |
| 215 | + const leadWebToLead = row('crm_lead', 'web_to_lead', 'form', { |
| 216 | + type: 'simple', |
| 217 | + data: leadData, |
| 218 | + sections: [{ name: 'contact_us', label: 'Contact us', fields: [{ field: 'name' }] }], |
| 219 | + sharing: { enabled: true, allowAnonymous: true, publicLink: '/forms/contact-us' }, |
| 220 | + }, { order: 2 }); |
| 221 | + const leadForm = row('crm_lead', 'form', 'form', { |
| 222 | + type: 'simple', |
| 223 | + sections: [{ name: 'lead_information', label: 'Lead Information', fields: [{ field: 'name' }] }], |
| 224 | + }, { order: 3, isDefault: true }); |
| 225 | + |
| 226 | + // examples/app-showcase `showcase_contact`: a named `create` form, then the |
| 227 | + // container's own `form`, the one default. |
| 228 | + const contactCreate = row('showcase_contact', 'create', 'form', { |
| 229 | + type: 'simple', |
| 230 | + sections: [{ name: 'quick', label: 'Quick add', fields: [{ field: 'first_name' }] }], |
| 231 | + }, { order: 1 }); |
| 232 | + const contactForm = row('showcase_contact', 'form', 'form', { |
| 233 | + type: 'simple', |
| 234 | + sections: [{ name: 'identity', label: 'Identity', fields: [{ field: 'first_name' }] }], |
| 235 | + }, { order: 2, isDefault: true }); |
| 236 | + |
| 237 | + const objects = [ |
| 238 | + { name: 'showcase_inquiry', fields: {} }, |
| 239 | + { name: 'crm_lead', fields: {} }, |
| 240 | + { name: 'showcase_contact', fields: {} }, |
| 241 | + ]; |
| 242 | + const byName = (merged: any[], name: string) => merged.find((o) => o.name === name); |
| 243 | + |
| 244 | + it('a container with only named forms (`showcase_inquiry`) gets no `.form`; its named form stays reachable by name', () => { |
| 245 | + const inquiry = byName(mergeViewsIntoObjects(objects, inquiryRows), 'showcase_inquiry'); |
| 246 | + expect(inquiry).not.toHaveProperty('form'); |
| 247 | + expect(Object.keys(inquiry.formViews)).toEqual(['showcase_inquiry.contact']); |
| 248 | + expect(inquiry.formViews['showcase_inquiry.contact'].isDefault).toBe(false); |
| 249 | + // The list family keeps its own default. |
| 250 | + expect(inquiry.list.name).toBe('showcase_inquiry.default'); |
| 251 | + }); |
| 252 | + |
| 253 | + it('create and edit take the no-default-form path for `showcase_inquiry`: no layout comes from the named form', () => { |
| 254 | + const inquiry = byName(mergeViewsIntoObjects(objects, inquiryRows), 'showcase_inquiry'); |
| 255 | + // The read `RecordFormPage`, `ScreenView` and `resolveFormViewLayout` share |
| 256 | + // (`.form`, then `formViews.default`) finds nothing… |
| 257 | + expect(inquiry.form ?? inquiry.formViews?.default).toBeUndefined(); |
| 258 | + // …so the New/Edit dialog (AppContent, `useActionModal`) gets no sections, |
| 259 | + // the same answer as an object that declares no form view at all. |
| 260 | + expect(resolveFormViewLayout(inquiry)).toEqual({}); |
| 261 | + expect(resolveFormViewLayout(inquiry)).toEqual(resolveFormViewLayout({ name: 'showcase_inquiry' } as any)); |
| 262 | + }); |
| 263 | + |
| 264 | + it('`crm_lead` keeps its default when the default arrives AFTER the named Web-to-Lead form', () => { |
| 265 | + const lead = byName(mergeViewsIntoObjects(objects, [leadWebToLead, leadForm]), 'crm_lead'); |
| 266 | + expect(lead.form.name).toBe('crm_lead.form'); |
| 267 | + expect(resolveFormViewLayout(lead).sections?.map((s: any) => s.name)).toEqual(['lead_information']); |
| 268 | + expect(lead.formViews['crm_lead.web_to_lead'].isDefault).toBe(false); |
| 269 | + }); |
| 270 | + |
| 271 | + it('`showcase_contact` keeps its default whichever order its two forms arrive in', () => { |
| 272 | + for (const rows of [[contactCreate, contactForm], [contactForm, contactCreate]]) { |
| 273 | + const contact = byName(mergeViewsIntoObjects(objects, rows), 'showcase_contact'); |
| 274 | + expect(contact.form.name).toBe('showcase_contact.form'); |
| 275 | + expect(Object.keys(contact.formViews).sort()).toEqual(['showcase_contact.create', 'showcase_contact.form']); |
| 276 | + } |
| 277 | + }); |
| 278 | + |
| 279 | + it('a named form arriving AFTER the default does not displace it', () => { |
| 280 | + const lead = byName(mergeViewsIntoObjects(objects, [leadForm, leadWebToLead]), 'crm_lead'); |
| 281 | + expect(lead.form.name).toBe('crm_lead.form'); |
150 | 282 | }); |
151 | 283 | }); |
152 | 284 |
|
|
0 commit comments