From 39cc4f4c2ebcc411699ab4b72864c586b60e6c5b Mon Sep 17 00:00:00 2001 From: xuejiahao Date: Fri, 17 Oct 2025 14:39:34 +0800 Subject: [PATCH] refactor --- background.js | 58 +++++--- constants.js | 70 ++++++++++ dom.js | 143 +++++++++++--------- fields.js | 360 +++++++++++++++++++++++++++++--------------------- main.js | 255 ++++++++++++++++++++++++----------- 5 files changed, 580 insertions(+), 306 deletions(-) create mode 100644 constants.js diff --git a/background.js b/background.js index 83f772d..25962fe 100644 --- a/background.js +++ b/background.js @@ -1,22 +1,42 @@ -chrome.action.onClicked.addListener(async (tab) => { +/** + * Injects the content scripts into the active tab to enable Kintone helper functionality + * @param {Object} tab - The active tab object from Chrome API + * @returns {Promise} Resolves when all scripts are injected successfully + */ +const injectKintoneHelperScripts = async (tab) => { + const scriptFiles = ['fields.js', 'dom.js', 'main.js']; + try { - // Inject the modules and then execute the main function - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ["fields.js"], - world: "MAIN" - }); - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ["dom.js"], - world: "MAIN" - }); - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ["main.js"], - world: "MAIN" - }); + // Inject all scripts in parallel for better performance + await Promise.all( + scriptFiles.map(file => + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: [file], + world: 'MAIN', + }) + ) + ); + + console.log('Kintone helper scripts injected successfully'); } catch (error) { - console.error("Error injecting scripts:", error); + console.error('Failed to inject Kintone helper scripts:', error); + throw error; // Re-throw to allow caller to handle if needed } -}); +}; + +/** + * Handles the extension action click event + * @param {Object} tab - The active tab that triggered the action + */ +const handleActionClick = async (tab) => { + try { + await injectKintoneHelperScripts(tab); + } catch (error) { + // Error already logged in injectKintoneHelperScripts + // Could add user notification here if needed + } +}; + +// Register the click handler for the browser action +chrome.action.onClicked.addListener(handleActionClick); diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..7d9faa3 --- /dev/null +++ b/constants.js @@ -0,0 +1,70 @@ +// Field type constants +export const FIELD_TYPES = { + SINGLE_LINE_TEXT: 'SINGLE_LINE_TEXT', + NUMBER: 'NUMBER', + MULTI_LINE_TEXT: 'MULTI_LINE_TEXT', + RICH_TEXT: 'RICH_TEXT', + LINK: 'LINK', + CHECK_BOX: 'CHECK_BOX', + RADIO_BUTTON: 'RADIO_BUTTON', + DROP_DOWN: 'DROP_DOWN', + MULTI_SELECT: 'MULTI_SELECT', + DATE: 'DATE', + TIME: 'TIME', + DATETIME: 'DATETIME', + USER_SELECT: 'USER_SELECT', + ORGANIZATION_SELECT: 'ORGANIZATION_SELECT', + GROUP_SELECT: 'GROUP_SELECT', + CALC: 'CALC', + RECORD_NUMBER: 'RECORD_NUMBER', + CREATOR: 'CREATOR', + CREATED_TIME: 'CREATED_TIME', + MODIFIER: 'MODIFIER', + UPDATED_TIME: 'UPDATED_TIME', + STATUS: 'STATUS', + STATUS_ASSIGNEE: 'STATUS_ASSIGNEE', + CATEGORY: 'CATEGORY', + FILE: 'FILE', + SUBTABLE: 'SUBTABLE', + GROUP: 'GROUP', + REFERENCE_TABLE: 'REFERENCE_TABLE', +}; + +// Layout types +export const LAYOUT_TYPES = { + ROW: 'ROW', + SUBTABLE: 'SUBTABLE', + GROUP: 'GROUP', + LABEL: 'LABEL', + HR: 'HR', + SPACER: 'SPACER', +}; + +// Field types that support option sorting +export const OPTION_SORTABLE_TYPES = [ + FIELD_TYPES.CHECK_BOX, + FIELD_TYPES.DROP_DOWN, + FIELD_TYPES.MULTI_SELECT, + FIELD_TYPES.RADIO_BUTTON, +]; + +// Colors +export const COLORS = { + TOOLTIP_TEXT: 'red', + SPACER_BORDER: '1px dotted red', +}; + +// Spacing and dimensions +export const SPACING = { + GROUP_MARGIN_LEFT: '20px', + REFERENCE_TABLE_SPACER: '30px', +}; + +// Field types excluded from group processing +export const EXCLUDED_GROUP_TYPES = [ + FIELD_TYPES.CATEGORY, + FIELD_TYPES.STATUS, + FIELD_TYPES.STATUS_ASSIGNEE, + FIELD_TYPES.SUBTABLE, + FIELD_TYPES.GROUP, +]; diff --git a/dom.js b/dom.js index 68059a3..14e8e23 100644 --- a/dom.js +++ b/dom.js @@ -1,91 +1,112 @@ +import { COLORS, SPACING, FIELD_TYPES } from './constants.js'; + +/** + * Extracts the guest space ID from the current URL + * @returns {string|undefined} Guest space ID or undefined if not found + */ export const getGuestSpaceId = () => { - const pathMatch = window.location.pathname.match(/\/guest\/([0-9]+)\//); - return pathMatch ? pathMatch[1] : undefined; + const match = window.location.pathname.match(/\/guest\/([0-9]+)\//); + return match ? match[1] : undefined; }; -export const createFieldSpan = ({ - code: fieldCode, - type: fieldType, - width: fieldWidth, -}) => { - const container = document.createElement("div"); - if (fieldWidth) { - container.style.width = `${Number(fieldWidth) - 8}px`; - container.style.marginLeft = "8px"; +/** + * Creates a field span element with proper styling + * @param {Object} params - Parameters for the span + * @param {string} params.code - Field code + * @param {string} params.type - Field type + * @param {number} params.width - Field width (optional) + * @returns {HTMLElement} Container div with span + */ +const createFieldSpan = ({ code, type, width }) => { + const container = document.createElement('div'); + + // Handle width and margin + if (width !== undefined) { + container.style.width = `${Number(width) - 8}px`; + container.style.marginLeft = '8px'; } else { - container.style.width = "100%"; + container.style.width = '100%'; } - const fieldSpan = document.createElement("span"); - fieldSpan.textContent = - fieldType !== void 0 ? `${fieldCode} (${fieldType})` : fieldCode; - fieldSpan.style.display = "inline-block"; - fieldSpan.style.width = "100%"; - fieldSpan.style.color = "red"; - fieldSpan.style.overflowWrap = "anywhere"; - fieldSpan.style.whiteSpace = "pre-wrap"; + // Create and style the span element + const fieldSpan = document.createElement('span'); + fieldSpan.textContent = type !== undefined ? `${code} (${type})` : code; + fieldSpan.style.display = 'inline-block'; + fieldSpan.style.width = '100%'; + fieldSpan.style.color = COLORS.TOOLTIP_TEXT; + fieldSpan.style.overflowWrap = 'anywhere'; + fieldSpan.style.whiteSpace = 'pre-wrap'; - if (fieldType === "GROUP") { - fieldSpan.style.marginLeft = "20px"; + // Special styling for GROUP type fields + if (type === FIELD_TYPES.GROUP) { + fieldSpan.style.marginLeft = SPACING.GROUP_MARGIN_LEFT; } container.appendChild(fieldSpan); return container; }; -export const createFieldWithTooltips = ({ - code: tooltipCode, - type: tooltipType, - width: tooltipWidth, -}) => { - const container = document.createElement("div"); - container.style.display = "inline-block"; +/** + * Creates a label container element + * @param {Object} params - Parameters for the label + * @param {string} params.code - Field code for label + * @param {string} params.type - Field type for label + * @param {number} params.width - Width for label container + * @returns {HTMLElement} Container with field span + */ +export const createFieldWithLabels = ({ code, type, width }) => { + const container = document.createElement('div'); + container.style.display = 'inline-block'; - const fieldSpan = createFieldSpan({ - code: tooltipCode, - type: tooltipType, - width: tooltipWidth, - }); - - container.append(fieldSpan); + const fieldSpan = createFieldSpan({ code, type, width }); + container.appendChild(fieldSpan); return container; }; +/** + * Creates a container with multiple field labels + * @param {Array} fieldNames - Names of fields to create labels for + * @param {Array} widths - Widths corresponding to field names + * @param {string} fieldType - Type of the field (affects spacing) + * @returns {HTMLElement} Span container with all labels + */ export const createFieldLabels = (fieldNames, widths, fieldType) => { - const labels = fieldNames.map((name, index) => { - const width = widths[index]; - return createFieldWithTooltips({ - code: name, - width: index === fieldNames.length - 1 ? width - 1 : width, - }); - }); + const container = document.createElement('span'); - const container = document.createElement("span"); - - if (fieldType === "REFERENCE_TABLE") { - const spacer = document.createElement("span"); - spacer.style.width = "30px"; - spacer.style.display = "inline-block"; + // Add spacer for reference table fields + if (fieldType === FIELD_TYPES.REFERENCE_TABLE) { + const spacer = document.createElement('span'); + spacer.style.width = SPACING.REFERENCE_TABLE_SPACER; + spacer.style.display = 'inline-block'; container.appendChild(spacer); } - for (let i = 0; i < labels.length; i++) { - const label = labels[i]; - container.appendChild(label); - } + // Create label elements with adjusted widths + const labels = fieldNames.map((name, index) => { + const adjustedWidth = index === fieldNames.length - 1 ? widths[index] - 1 : widths[index]; + return createFieldWithLabels({ + code: name, + width: adjustedWidth, + }); + }); + + // Append all labels to container + labels.forEach(label => container.appendChild(label)); return container; }; +/** + * Extracts column widths from table headers + * @param {HTMLElement} tableElement - Table element to analyze + * @param {string} fieldType - Type of field (affects returned widths) + * @returns {Array} Array of column widths + */ export const getColumnWidths = (tableElement, fieldType) => { - const columnWidths = []; - const headers = tableElement.querySelectorAll("th"); + const headers = Array.from(tableElement.querySelectorAll('th')); + const widths = headers.map(header => header.clientWidth); - for (let i = 0; i < headers.length; i++) { - const header = headers[i]; - columnWidths.push(header.clientWidth); - } - - return fieldType === "SUBTABLE" ? columnWidths : columnWidths.slice(1); + // For subtables include all widths, for others skip first column + return fieldType === FIELD_TYPES.SUBTABLE ? widths : widths.slice(1); }; diff --git a/fields.js b/fields.js index 68a3511..e7c3de9 100644 --- a/fields.js +++ b/fields.js @@ -1,160 +1,224 @@ +import { + FIELD_TYPES, + LAYOUT_TYPES, + OPTION_SORTABLE_TYPES, + EXCLUDED_GROUP_TYPES +} from './constants.js'; + +/** + * Sorts field options by index for fields that support options + * @param {Object} field - Field object + * @returns {Array} Sorted option labels + */ +const sortFieldOptions = (field) => { + if (!field.options) return []; + return Object.values(field.options) + .sort((a, b) => Number(a.index) - Number(b.index)) + .map(option => option.label); +}; + +/** + * Checks if a field should have its options sorted + * @param {string} fieldType - Type of the field + * @returns {boolean} True if options should be sorted + */ +const shouldSortOptions = (fieldType) => OPTION_SORTABLE_TYPES.includes(fieldType); + +/** + * Adds system status fields to the fields array if they are enabled + * @param {Array} properties - All property objects + * @param {Array} fields - Array to add fields to + */ +const addSystemStatusFields = (properties, fields) => { + const propertyList = Object.values(properties); + + [FIELD_TYPES.STATUS, FIELD_TYPES.STATUS_ASSIGNEE, FIELD_TYPES.CATEGORY].forEach(type => { + const field = propertyList.find(f => f.type === type); + if (field && field.enabled) { + fields.push(field); + } + }); +}; + +/** + * Processes fields in a regular row layout + * @param {Array} rowFields - Fields in the row + * @param {Object} properties - Property mapping + * @param {Array} fields - Array to add processed fields to + * @param {Array} spacers - Array to add spacer elements to + */ +const processRowFields = (rowFields, properties, fields, spacers) => { + for (const field of rowFields) { + if ([LAYOUT_TYPES.LABEL, LAYOUT_TYPES.HR].includes(field.type)) continue; + + if (field.type === LAYOUT_TYPES.SPACER) { + spacers.push(field); + continue; + } + + const fieldProperty = properties[field.code]; + if ([FIELD_TYPES.SUBTABLE, FIELD_TYPES.GROUP].includes(fieldProperty.type)) continue; + + const fieldToAdd = shouldSortOptions(fieldProperty.type) + ? { ...fieldProperty, sortedOptions: sortFieldOptions(fieldProperty) } + : fieldProperty; + + fields.push(fieldToAdd); + } +}; + +/** + * Processes subtable fields + * @param {Object} layout - Subtable layout object + * @param {Object} properties - Property mapping + * @param {Array} fields - Array to add processed fields to + */ +const processSubtableFields = (layout, properties, fields) => { + const tableCode = layout.code; + const tableField = properties[tableCode]; + + if (tableField.type !== FIELD_TYPES.SUBTABLE) return; + + const tableFields = {}; + + layout.fields.forEach(({ code: fieldCode }) => { + const subField = tableField.fields[fieldCode]; + const processedField = shouldSortOptions(subField.type) + ? { ...subField, table: tableCode, sortedOptions: sortFieldOptions(subField) } + : { ...subField, table: tableCode }; + + tableFields[fieldCode] = processedField; + fields.push(processedField); + }); + + fields.push({ ...tableField, fields: tableFields }); +}; + +/** + * Processes group fields + * @param {Object} layout - Group layout object + * @param {Object} properties - Property mapping + * @param {Array} fields - Array to add processed fields to + * @param {Array} spacers - Array to add spacer elements to + */ +const processGroupFields = (layout, properties, fields, spacers) => { + const groupCode = layout.code; + const groupField = properties[groupCode]; + + if (groupField.type !== FIELD_TYPES.GROUP) return; + + const groupFields = {}; + + layout.layout.forEach(row => { + row.fields.forEach(field => { + if ([LAYOUT_TYPES.LABEL, LAYOUT_TYPES.HR].includes(field.type)) return; + + if (field.type === LAYOUT_TYPES.SPACER) { + spacers.push({ ...field, group: groupCode }); + return; + } + + const groupSubField = properties[field.code]; + + if (EXCLUDED_GROUP_TYPES.includes(groupSubField.type)) return; + + const processedField = shouldSortOptions(groupSubField.type) + ? { ...groupSubField, group: groupCode, sortedOptions: sortFieldOptions(groupSubField) } + : { ...groupSubField, group: groupCode }; + + groupFields[field.code] = processedField; + fields.push(processedField); + }); + }); + + fields.push({ ...groupField, fields: groupFields }); +}; + +/** + * Marks lookup copy fields in the fields array + * @param {Array} fields - Array of fields to process + */ +const markLookupCopies = (fields) => { + const lookupCopyTypes = [ + FIELD_TYPES.SINGLE_LINE_TEXT, FIELD_TYPES.NUMBER, FIELD_TYPES.MULTI_LINE_TEXT, + FIELD_TYPES.RICH_TEXT, FIELD_TYPES.LINK, FIELD_TYPES.CHECK_BOX, + FIELD_TYPES.RADIO_BUTTON, FIELD_TYPES.DROP_DOWN, FIELD_TYPES.MULTI_SELECT, + FIELD_TYPES.DATE, FIELD_TYPES.TIME, FIELD_TYPES.DATETIME, + FIELD_TYPES.USER_SELECT, FIELD_TYPES.ORGANIZATION_SELECT, + FIELD_TYPES.GROUP_SELECT, FIELD_TYPES.CALC, FIELD_TYPES.RECORD_NUMBER + ]; + + fields.forEach(field => { + if (field.lookup && Array.isArray(field.lookup.fieldMappings)) { + field.lookup.fieldMappings.forEach(mapping => { + const targetField = fields.find(f => + lookupCopyTypes.includes(f.type) && f.code === mapping.field + ); + if (targetField) { + targetField.isLookupCopy = true; + } + }); + } + }); +}; + +/** + * Adds system fields that aren't already in the fields array + * @param {Array} properties - Property objects + * @param {Array} fields - Array to add fields to + */ +const addSystemFields = (properties, fields) => { + const propertyList = Object.values(properties); + const systemFieldTypes = [ + FIELD_TYPES.RECORD_NUMBER, + FIELD_TYPES.CREATOR, + FIELD_TYPES.CREATED_TIME, + FIELD_TYPES.MODIFIER, + FIELD_TYPES.UPDATED_TIME + ]; + + systemFieldTypes.forEach(type => { + const field = propertyList.find(f => f.type === type); + if (field && !fields.some(f => f.type === type)) { + fields.push(field); + } + }); +}; + export const kintonePrettyFields = { + /** + * Generates processed field and spacer arrays from form properties and layout + * @param {Object} properties - Field properties object + * @param {Array} layouts - Layout definitions + * @returns {Object} Object containing fields and spacers arrays + */ generateFields: (properties, layouts) => { const fields = []; const spacers = []; - const isStatus = field => field.type === "STATUS"; - const isStatusAssignee = field => field.type === "STATUS_ASSIGNEE"; - const isCategory = field => field.type === "CATEGORY"; + // Add system status fields first + addSystemStatusFields(properties, fields); - const propertyList = Object.values(properties) - - const statusField = propertyList.find(isStatus); - if (statusField && statusField.enabled) { - fields.push(statusField); - } - - const statusAssigneeField = propertyList.find(isStatusAssignee); - if (statusAssigneeField && statusAssigneeField.enabled) { - fields.push(statusAssigneeField); - } - - const categoryField = propertyList.find(isCategory); - if (categoryField && categoryField.enabled) { - fields.push(categoryField); - } - - for (const r of layouts) { - if (r.type === "ROW") { - for (const f of r.fields) { - if (f.type === "LABEL" || f.type === "HR") continue; - if (f.type === "SPACER") { - spacers.push(f); - continue; - } - const ee = properties[f.code]; - if (ee.type !== "SUBTABLE" && ee.type !== "GROUP") { - if (ee.type === "CHECK_BOX" || ee.type === "DROP_DOWN" || ee.type === "MULTI_SELECT" || ee.type === "RADIO_BUTTON") { - const sortedOptions = Object.values(ee.options).sort((a, b) => Number(a.index) - Number(b.index)).map(o => o.label); - fields.push({ - ...ee, - sortedOptions - }); - continue; - } - fields.push(ee); - } - } - } else if (r.type === "SUBTABLE") { - const tableCode = r.code; - const tableField = properties[tableCode]; - if (tableField.type !== "SUBTABLE") continue; - const tableFields = {}; - for (const { code: fieldCode } of r.fields) { - const subf = tableField.fields[fieldCode]; - if (subf.type === "CHECK_BOX" || subf.type === "DROP_DOWN" || subf.type === "MULTI_SELECT" || subf.type === "RADIO_BUTTON") { - const sortedOptions = Object.values(subf.options).sort((a, b) => Number(a.index) - Number(b.index)).map(o => o.label); - tableFields[fieldCode] = { - ...subf, - table: tableCode, - sortedOptions - }; - fields.push({ - ...subf, - table: tableCode, - sortedOptions - }); - continue; - } - tableFields[fieldCode] = { - ...subf, - table: tableCode - }; - fields.push({ - ...subf, - table: tableCode - }); - } - fields.push({ - ...tableField, - fields: tableFields - }); - } else if (r.type === "GROUP") { - const groupCode = r.code; - const groupField = properties[groupCode]; - if (groupField.type !== "GROUP") continue; - const groupFields = {}; - for (const row of r.layout) { - for (const f of row.fields) { - if (f.type === "LABEL" || f.type === "HR") continue; - if (f.type === "SPACER") { - spacers.push({ - ...f, - group: groupCode - }); - continue; - } - const gSubf = properties[f.code]; - if (gSubf.type !== "CATEGORY" && gSubf.type !== "STATUS" && gSubf.type !== "STATUS_ASSIGNEE" && gSubf.type !== "SUBTABLE" && gSubf.type !== "GROUP") { - if (gSubf.type === "CHECK_BOX" || gSubf.type === "DROP_DOWN" || gSubf.type === "MULTI_SELECT" || gSubf.type === "RADIO_BUTTON") { - const sortedOptions = Object.values(gSubf.options).sort((a, b) => Number(a.index) - Number(b.index)).map(o => o.label); - groupFields[f.code] = { - ...gSubf, - group: groupCode, - sortedOptions - }; - fields.push({ - ...gSubf, - group: groupCode, - sortedOptions - }); - continue; - } - groupFields[f.code] = { - ...gSubf, - group: groupCode - }; - fields.push({ - ...gSubf, - group: groupCode - }); - } - } - } - fields.push({ - ...groupField, - fields: groupFields - }); + // Process each layout item + layouts.forEach(layout => { + switch (layout.type) { + case LAYOUT_TYPES.ROW: + processRowFields(layout.fields, properties, fields, spacers); + break; + case LAYOUT_TYPES.SUBTABLE: + processSubtableFields(layout, properties, fields); + break; + case LAYOUT_TYPES.GROUP: + processGroupFields(layout, properties, fields, spacers); + break; } - } + }); - // lookup copy - const flatFields = fields; - for (const f of flatFields) { - if ('lookup' in f && f.lookup !== null) { - for (const mapping of f.lookup.fieldMappings) { - const target = flatFields.find(ee => ["SINGLE_LINE_TEXT", "NUMBER", "MULTI_LINE_TEXT", "RICH_TEXT", "LINK", "CHECK_BOX", "RADIO_BUTTON", "DROP_DOWN", "MULTI_SELECT", "DATE", "TIME", "DATETIME", "USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT", "CALC", "RECORD_NUMBER"].includes(ee.type) && ee.code === mapping.field); - if (target) { - target.isLookupCopy = true; - } - } - } - } + // Mark lookup copy fields + markLookupCopies(fields); - const recordNumberField = propertyList.find(f => f.type === "RECORD_NUMBER"); - if (recordNumberField && !fields.some(f => f.type === "RECORD_NUMBER")) fields.push(recordNumberField); - - const creatorField = propertyList.find(f => f.type === "CREATOR"); - if (creatorField && !fields.some(f => f.type === "CREATOR")) fields.push(creatorField); - - const createdTimeField = propertyList.find(f => f.type === "CREATED_TIME"); - if (createdTimeField && !fields.some(f => f.type === "CREATED_TIME")) fields.push(createdTimeField); - - const modifierField = propertyList.find(f => f.type === "MODIFIER"); - if (modifierField && !fields.some(f => f.type === "MODIFIER")) fields.push(modifierField); - - const updatedTimeField = propertyList.find(f => f.type === "UPDATED_TIME"); - if (updatedTimeField && !fields.some(f => f.type === "UPDATED_TIME")) fields.push(updatedTimeField); + // Add any missing system fields + addSystemFields(properties, fields); return { fields, spacers }; }, diff --git a/main.js b/main.js index e5ad1e1..3b9a363 100644 --- a/main.js +++ b/main.js @@ -1,93 +1,192 @@ import { kintonePrettyFields } from './fields.js'; -import { getGuestSpaceId, createFieldWithTooltips, createFieldLabels, getColumnWidths } from './dom.js'; +import { getGuestSpaceId, createFieldWithLabels, createFieldLabels, getColumnWidths } from './dom.js'; import { KintoneRestAPIClient } from '@kintone/rest-api-client'; +import { COLORS } from './constants.js'; -export async function runKintoneHelper() { - const guestSpaceId = getGuestSpaceId(); +/** + * Fetches form data from Kintone API + * @param {number} appId - The application ID + * @param {string} language - User language setting + * @returns {Promise} Form fields and layout data + */ +const fetchFormData = async (appId, language) => { const client = new KintoneRestAPIClient({ - guestSpaceId: guestSpaceId, + guestSpaceId: getGuestSpaceId(), }); - const appId = kintone.app.getId(); - if (appId === null) return; - - const language = kintone.getLoginUser().language; const isPreview = false; - const { properties: formFields } = await client.app.getFormFields({ - app: appId, - lang: language, - preview: isPreview, - }); - const { layout: layout } = await client.app.getFormLayout({ - app: appId, - preview: isPreview, - }); - const { fields: fieldsWithLabels, spacers: spacerElements } = - kintonePrettyFields.generateFields(formFields, layout); + + try { + const [formFieldsResult, layoutResult] = await Promise.all([ + client.app.getFormFields({ + app: appId, + lang: language, + preview: isPreview, + }), + client.app.getFormLayout({ + app: appId, + preview: isPreview, + }), + ]); + + return { + formFields: formFieldsResult.properties, + layout: layoutResult.layout, + }; + } catch (error) { + console.error('Failed to fetch form data:', error); + throw new Error('Could not retrieve form configuration'); + } +}; + +/** + * Adds label to a subtable field element + * @param {Object} field - Field configuration + * @param {HTMLElement} fieldElement - The field DOM element + */ +const addSubtableLabel = (field, fieldElement) => { + // Add field code label above the table + fieldElement.before( + createFieldWithLabels({ + code: field.code, + type: field.type, + }) + ); + + // Add column labels below the table + const fieldNames = Object.keys(field.fields); + const columnWidths = getColumnWidths(fieldElement, field.type); + fieldElement.after( + createFieldLabels(fieldNames, columnWidths, field.type) + ); +}; + +/** + * Adds label to a reference table field element + * @param {Object} field - Field configuration + * @param {HTMLElement} fieldElement - The field DOM element + */ +const addReferenceTableLabel = (field, fieldElement) => { + // Add field code label above the reference table + fieldElement.before( + createFieldWithLabels({ + code: field.code, + type: field.type, + }) + ); + + // Add display field labels if available + if (field.referenceTable?.displayFields) { + const displayFields = field.referenceTable.displayFields; + const columnWidths = getColumnWidths(fieldElement, field.type); + fieldElement.appendChild( + createFieldLabels(displayFields, columnWidths, field.type) + ); + } +}; + +/** + * Adds label to a group field element + * @param {Object} field - Field configuration + * @param {HTMLElement} fieldElement - The field DOM element + */ +const addGroupLabel = (field, fieldElement) => { + fieldElement.parentElement?.before( + createFieldWithLabels({ + code: field.code, + type: field.type, + }) + ); +}; + +/** + * Adds label to a standard field element + * @param {Object} field - Field configuration + * @param {HTMLElement} fieldElement - The field DOM element + */ +const addStandardFieldLabel = (field, fieldElement) => { + fieldElement.before( + createFieldWithLabels({ + code: field.code, + type: field.type, + }) + ); +}; + +/** + * Processes and adds labels to all field elements on the page + * @param {Array} fieldsWithLabels - Processed field objects with label info + */ +const processFieldLabels = (fieldsWithLabels) => { for (const field of fieldsWithLabels) { const fieldElement = kintone.app.record.getFieldElement(field.code); - if (fieldElement) { - if (kintonePrettyFields.isSubtable(field)) { - fieldElement.before( - createFieldWithTooltips({ - code: field.code, - type: field.type, - }) - ); - const fieldNames = Object.keys(field.fields); - const columnWidths = getColumnWidths(fieldElement, field.type); - fieldElement.after( - createFieldLabels(fieldNames, columnWidths, field.type) - ); - continue; - } + if (!fieldElement) continue; - if (kintonePrettyFields.isReferenceTable(field)) { - fieldElement.before( - createFieldWithTooltips({ - code: field.code, - type: field.type, - }) - ); - - if (field.referenceTable) { - const displayFields = field.referenceTable.displayFields; - const columnWidths = getColumnWidths(fieldElement, field.type); - fieldElement.appendChild( - createFieldLabels(displayFields, columnWidths, field.type) - ); - } - continue; - } - if (kintonePrettyFields.isGroup(field)) { - fieldElement.parentElement?.before( - createFieldWithTooltips({ - code: field.code, - type: field.type, - }), - ); - continue; - } - fieldElement.before( - createFieldWithTooltips({ - code: field.code, - type: field.type, - }), - ); + // Handle different field types with appropriate label placement + if (kintonePrettyFields.isSubtable(field)) { + addSubtableLabel(field, fieldElement); + } else if (kintonePrettyFields.isReferenceTable(field)) { + addReferenceTableLabel(field, fieldElement); + } else if (kintonePrettyFields.isGroup(field)) { + addGroupLabel(field, fieldElement); + } else { + addStandardFieldLabel(field, fieldElement); } } +}; + +/** + * Processes and adds labels to spacer elements on the page + * @param {Array} spacerElements - Spacer element configurations + */ +const processSpacerLabels = (spacerElements) => { for (const spacer of spacerElements) { const spacerElement = kintone.app.record.getSpaceElement(spacer.elementId); - if (spacerElement) { - spacerElement.appendChild( - createFieldWithTooltips({ - code: spacer.elementId, - }), - ); - spacerElement.style.border = "1px dotted blue"; - } - } -} + if (!spacerElement) continue; -// Execute immediately upon injection -runKintoneHelper().catch(console.error); + // Add label and blue border to spacer elements + spacerElement.appendChild( + createFieldWithLabels({ + code: spacer.elementId, + }) + ); + spacerElement.style.border = COLORS.SPACER_BORDER; + } +}; + +/** + * Main function to enhance Kintone forms with labels and visual helpers + * @returns {Promise} + */ +export const runKintoneHelper = async () => { + try { + // Early return if not in a valid app context + const appId = kintone.app.getId(); + if (!appId) { + console.log('Not in a valid Kintone app context'); + return; + } + + // Fetch necessary form configuration + const language = kintone.getLoginUser().language; + const { formFields, layout } = await fetchFormData(appId, language); + + // Process fields and generate label data + const { fields: fieldsWithLabels, spacers: spacerElements } = + kintonePrettyFields.generateFields(formFields, layout); + + // Apply labels to field elements + processFieldLabels(fieldsWithLabels); + + // Apply visual indicators to spacer elements + processSpacerLabels(spacerElements); + + console.log('Kintone helper labels applied successfully'); + } catch (error) { + console.error('Failed to run Kintone helper:', error); + // Could implement user-friendly error notification here + } +}; + +// Execute helper function immediately when the script is injected +runKintoneHelper();