diff --git a/background.js b/background.js index 25962fe..9aa709d 100644 --- a/background.js +++ b/background.js @@ -1,42 +1,34 @@ /** - * 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 + * Chrome 扩展后台脚本 + * 负责将内容脚本注入 Kintone 页面 + */ + +import { SCRIPT_FILES } from './utils/constants.js'; + +/** + * 将 Kintone Helper 脚本注入指定标签页 + * @param {Object} tab - Chrome 标签页对象 + * @returns {Promise} 当脚本注入成功时解决 */ const injectKintoneHelperScripts = async (tab) => { - const scriptFiles = ['fields.js', 'dom.js', 'main.js']; - try { - // Inject all scripts in parallel for better performance + // 将所有脚本并行注入 await Promise.all( - scriptFiles.map(file => + SCRIPT_FILES.map(scriptFile => chrome.scripting.executeScript({ target: { tabId: tab.id }, - files: [file], + files: [scriptFile], world: 'MAIN', }) ) ); - console.log('Kintone helper scripts injected successfully'); } catch (error) { - console.error('Failed to inject Kintone helper scripts:', error); - throw error; // Re-throw to allow caller to handle if needed + throw new Error(`Kintone Helper script injection failed: ${error.message}`); } }; -/** - * 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); +// 当点击扩展,开始执行脚本注入 +chrome.action.onClicked.addListener(async (tab) => { + await injectKintoneHelperScripts(tab); +}); diff --git a/constants.js b/constants.js deleted file mode 100644 index 7d9faa3..0000000 --- a/constants.js +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index 14e8e23..0000000 --- a/dom.js +++ /dev/null @@ -1,112 +0,0 @@ -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 match = window.location.pathname.match(/\/guest\/([0-9]+)\//); - return match ? match[1] : undefined; -}; - -/** - * 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%'; - } - - // 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'; - - // Special styling for GROUP type fields - if (type === FIELD_TYPES.GROUP) { - fieldSpan.style.marginLeft = SPACING.GROUP_MARGIN_LEFT; - } - - container.appendChild(fieldSpan); - return container; -}; - -/** - * 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, 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 container = document.createElement('span'); - - // 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); - } - - // 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 headers = Array.from(tableElement.querySelectorAll('th')); - const widths = headers.map(header => header.clientWidth); - - // For subtables include all widths, for others skip first column - return fieldType === FIELD_TYPES.SUBTABLE ? widths : widths.slice(1); -}; diff --git a/features/add-field-label/fields.js b/features/add-field-label/fields.js new file mode 100644 index 0000000..711f6ca --- /dev/null +++ b/features/add-field-label/fields.js @@ -0,0 +1,185 @@ +import { + LAYOUT_TYPES, + FIELD_TYPES, + SYSTEM_STATUS_FIELD_TYPES, + SYSTEM_FIELD_TYPES, + EXCLUDED_GROUP_TYPES +} from '../../utils/constants.js'; +import { + sortFieldOptions, + shouldSortOptions, + markLookupCopies, +} from '../../utils/field-utils.js'; + +/** + * 从属性列表中添加系统状态字段到字段数组中 + * @param {Object} properties - 所有字段属性对象 + * @param {Array} fields - 要添加字段的数组 + */ +const addSystemStatusFields = (properties, fields) => { + const propertyList = Object.values(properties); + + SYSTEM_STATUS_FIELD_TYPES.forEach(type => { + const field = propertyList.find(f => f.type === type); + if (field?.enabled) { + fields.push(field); + } + }); +}; + +/** + * 处理常规行布局中的字段 + * @param {Array} rowFields - 行中的字段列表 + * @param {Object} properties - 字段属性映射 + * @param {Array} fields - 要添加字段的数组 + * @param {Array} spacers - 要添加间距元素的数组 + */ +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 processedField = shouldSortOptions(fieldProperty.type) + ? { ...fieldProperty, sortedOptions: sortFieldOptions(fieldProperty) } + : fieldProperty; + + fields.push(processedField); + } +}; + +/** + * 处理子表字段 + * @param {Object} layout - 子表布局对象 + * @param {Object} properties - 字段属性映射 + * @param {Array} fields - 要添加字段的数组 + */ +const processSubtableFields = (layout, properties, fields) => { + const tableCode = layout.code; + const tableField = properties[tableCode]; + + if (tableField.type !== FIELD_TYPES.SUBTABLE) return; + + const subtableFieldsMap = {}; + + // 处理子表中每个字段 + layout.fields.forEach(({ code: fieldCode }) => { + const subField = tableField.fields[fieldCode]; + const processedField = shouldSortOptions(subField.type) + ? { ...subField, table: tableCode, sortedOptions: sortFieldOptions(subField) } + : { ...subField, table: tableCode }; + + subtableFieldsMap[fieldCode] = processedField; + fields.push(processedField); + }); + + // 将完整的子表信息也添加到字段列表中 + fields.push({ ...tableField, fields: subtableFieldsMap }); +}; + +/** + * 处理分组字段 + * @param {Object} layout - 分组布局对象 + * @param {Object} properties - 字段属性映射 + * @param {Array} fields - 要添加字段的数组 + * @param {Array} spacers - 要添加间距元素的数组 + */ +const processGroupFields = (layout, properties, fields, spacers) => { + const groupCode = layout.code; + const groupField = properties[groupCode]; + + if (groupField.type !== FIELD_TYPES.GROUP) return; + + const groupFieldsMap = {}; + + // 处理分组中的每个字段 + 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 }; + + groupFieldsMap[field.code] = processedField; + fields.push(processedField); + }); + }); + + // 将完整的分组信息也添加到字段列表中 + fields.push({ ...groupField, fields: groupFieldsMap }); +}; + +/** + * 添加缺失的系统字段 + * @param {Object} properties - 字段属性对象 + * @param {Array} fields - 要添加字段的数组 + */ +const addMissingSystemFields = (properties, fields) => { + const propertyList = Object.values(properties); + + SYSTEM_FIELD_TYPES.forEach(type => { + const field = propertyList.find(f => f.type === type); + // 只添加还没有的系统字段 + if (field && !fields.some(f => f.type === type)) { + fields.push(field); + } + }); +}; + +/** + * 从表单属性和布局生成处理后的字段和间距数组 + * @param {Object} properties - 字段属性对象 + * @param {Array} layouts - 布局定义 + * @returns {Object} 包含字段和间距数组的对象 + */ +export const generateFields = (properties, layouts) => { + const fields = []; + const spacers = []; + + // 首先添加系统状态字段 + addSystemStatusFields(properties, fields); + + // 处理每个布局项 + 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; + } + }); + + markLookupCopies(fields); + + addMissingSystemFields(properties, fields); + + return { fields, spacers }; +}; diff --git a/features/add-field-label/main.js b/features/add-field-label/main.js new file mode 100644 index 0000000..da3a104 --- /dev/null +++ b/features/add-field-label/main.js @@ -0,0 +1,106 @@ +/** + * 添加字段信息功能模块 + * 负责获取表单配置并添加字段信息标签 + */ + +import { generateFields } from './fields.js'; +import { getGuestSpaceId, getAppId, isInDetailPage, isInAdminFormPage } from '../../utils/kintone-utils.js'; +import { FieldLabelProcessor } from '../../page/detail/field-label-processor.js'; +import { AdminFieldLabelProcessor } from '../../page/admin/form/admin-field-label-processor.js'; +import { KintoneRestAPIClient } from '@kintone/rest-api-client'; +import { PAGE_TYPES } from '../../utils/constants.js'; + +/** + * 从 Kintone API 获取表单数据 + * @param {number} appId - 应用 ID + * @param {boolean} isPreview - 是否为预览模式 + * @returns {Promise} 表单字段和布局数据 + */ +const fetchFormData = async (appId, isPreview) => { + try { + // 初始化 Kintone REST API 客户端 + const client = new KintoneRestAPIClient({ + guestSpaceId: getGuestSpaceId(), // 访客空间 ID,支持在访客空间中工作 + }); + + const [formFieldsResult, layoutResult] = await Promise.all([ + client.app.getFormFields({ + app: appId, + preview: isPreview, + }), + client.app.getFormLayout({ + app: appId, + preview: isPreview, + }), + ]); + + if (!formFieldsResult?.properties) { + throw new Error('Failed to retrieve form field data'); + } + + if (!layoutResult?.layout) { + throw new Error('Failed to retrieve form layout data'); + } + + return { + formFields: formFieldsResult.properties, // 表单字段属性 + layout: layoutResult.layout, // 表单布局配置 + }; + } catch (error) { + console.error('Failed to fetch form data: ', error); + throw new Error(`Unable to retrieve form configuration: ${error.message}`); + } +}; + +/** + * 主函数:为 Kintone 表单添加字段信息标签 + * @returns {Promise} + */ +export const addFieldLabel = async () => { + try { + const appId = getAppId(); + if (!appId || typeof appId !== 'number') { + return; + } + + // 创建字段标签处理器实例 + const labelProcessor = getFieldLabelProcessor(appId); + if (!labelProcessor) { + console.warn('Unable to create field label processor') + return; + } + + // 从 API 获取表单配置数据 + const { formFields, layout } = await fetchFormData(appId, labelProcessor.isPreview()); + + // 处理字段并生成标签数据 + const { fields: fieldsWithLabels, spacers: spacerElements } = + generateFields(formFields, layout); + + console.log(`Processed ${fieldsWithLabels.length} fields and ${spacerElements.length} spacer elements`); + + // 字段元素 + labelProcessor.processFieldLabels(fieldsWithLabels); + // 间距元素 + labelProcessor.processSpacerLabels(spacerElements); + + } catch (error) { + console.error('Failed to add field information: ', error); + throw error; + } +}; + +const getFieldLabelProcessor = (appId) => { + if (isInDetailPage()) { + return new FieldLabelProcessor({ + appId, + pageType: PAGE_TYPES.DETAIL // 当前专注于详情页面 + }); + } + if (isInAdminFormPage()) { + return new AdminFieldLabelProcessor({ + appId, + pageType: PAGE_TYPES.ADMIN // admin 表单页面 + }); + } +}; diff --git a/features/add-field-label/settings.js b/features/add-field-label/settings.js new file mode 100644 index 0000000..d616364 --- /dev/null +++ b/features/add-field-label/settings.js @@ -0,0 +1,15 @@ +// 颜色配置常量 +export const COLORS = { + LABEL_TEXT: 'red', + SPACER_BORDER: '1px dotted red', +}; + +// 间距和尺寸配置常量 +export const SPACING = { + GROUP_MARGIN_LEFT: '20px', + REFERENCE_TABLE_SPACER: '30px', + TABLE_COLUMN_PADDING: 8, + FIELD_CONTAINER_WIDTH: '100%', +}; + +export let IS_FIELD_TYPE_DISPLAY = false; diff --git a/fields.js b/fields.js deleted file mode 100644 index e7c3de9..0000000 --- a/fields.js +++ /dev/null @@ -1,257 +0,0 @@ -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 = []; - - // Add system status fields first - addSystemStatusFields(properties, fields); - - // 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; - } - }); - - // Mark lookup copy fields - markLookupCopies(fields); - - // Add any missing system fields - addSystemFields(properties, fields); - - return { fields, spacers }; - }, - isCategory: field => field.type === "CATEGORY", - isCheckBox: field => field.type === "CHECK_BOX", - isCreatedTime: field => field.type === "CREATED_TIME", - isCreator: field => field.type === "CREATOR", - isDate: field => field.type === "DATE", - isDatetime: field => field.type === "DATETIME", - isDropDown: field => field.type === "DROP_DOWN", - isFile: field => field.type === "FILE", - isGroup: field => field.type === "GROUP", - isGroupSelect: field => field.type === "GROUP_SELECT", - isInGroup: field => 'group' in field, - isInSubtable: field => 'table' in field, - isLink: field => field.type === "LINK", - isLookup: field => 'lookup' in field, - isLookupCopy: field => field.isLookupCopy === true, - isModifier: field => field.type === "MODIFIER", - isMultiLineText: field => field.type === "MULTI_LINE_TEXT", - isMultiSelect: field => field.type === "MULTI_SELECT", - isNotInSubtable: field => !('table' in field), - isNumber: field => field.type === "NUMBER", - isOrganizationSelect: field => field.type === "ORGANIZATION_SELECT", - isRadioButton: field => field.type === "RADIO_BUTTON", - isRecordNumber: field => field.type === "RECORD_NUMBER", - isReferenceTable: field => field.type === "REFERENCE_TABLE", - isRichText: field => field.type === "RICH_TEXT", - isSingleLineText: field => field.type === "SINGLE_LINE_TEXT", - isStatus: field => field.type === "STATUS", - isStatusAssignee: field => field.type === "STATUS_ASSIGNEE", - isSubtable: field => field.type === "SUBTABLE", - isTime: field => field.type === "TIME", - isUpdatedTime: field => field.type === "UPDATED_TIME", - isUserSelect: field => field.type === "USER_SELECT", -}; diff --git a/main.js b/main.js index 3b9a363..49d5b98 100644 --- a/main.js +++ b/main.js @@ -1,192 +1,22 @@ -import { kintonePrettyFields } from './fields.js'; -import { getGuestSpaceId, createFieldWithLabels, createFieldLabels, getColumnWidths } from './dom.js'; -import { KintoneRestAPIClient } from '@kintone/rest-api-client'; -import { COLORS } from './constants.js'; - /** - * 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 + * Kintone Helper Extension 主入口点模块 + * 协调所有功能模块以增强 Kintone 系统体验 */ -const fetchFormData = async (appId, language) => { - const client = new KintoneRestAPIClient({ - guestSpaceId: getGuestSpaceId(), - }); - const isPreview = false; - - 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'); - } -}; +import { addFieldLabel } from './features/add-field-label/main.js'; /** - * 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) continue; - - // 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) continue; - - // 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 + * 运行主要的 Kintone Helper 函数 * @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'); + await addFieldLabel(); } catch (error) { - console.error('Failed to run Kintone helper:', error); - // Could implement user-friendly error notification here + console.error('Kintone Helper execution failed:', error); + throw error; } }; -// Execute helper function immediately when the script is injected +// 目前立即执行 runKintoneHelper(); diff --git a/package-lock.json b/package-lock.json index 1f2fe8e..9c80e97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ }, "devDependencies": { "@vitejs/plugin-legacy": "^6.1.1", + "cross-env": "^10.1.0", + "onchange": "^7.1.0", "vite": "^6.3.6", "vite-plugin-web-extension": "^4.4.5" } @@ -1541,6 +1543,20 @@ "node": ">=6.9.0" } }, + "node_modules/@blakeembrey/deque": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@blakeembrey/deque/-/deque-1.0.5.tgz", + "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@blakeembrey/template": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@blakeembrey/template/-/template-1.2.0.tgz", + "integrity": "sha512-w/63nURdkRPpg3AXbNr7lPv6HgOuVDyefTumiXsbXxtIwcuk5EXayWR5OpSwDjsQPgaYsfUSedMduaNOjAYY8A==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@devicefarmer/adbkit": { "version": "3.3.8", "resolved": "https://registry.npmmirror.com/@devicefarmer/adbkit/-/adbkit-3.3.8.tgz", @@ -1611,6 +1627,13 @@ } } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -2658,6 +2681,40 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/array-differ": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/array-differ/-/array-differ-4.0.0.tgz", @@ -2794,6 +2851,19 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", @@ -2855,6 +2925,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.26.3", "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.26.3.tgz", @@ -3001,6 +3084,31 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chrome-launcher": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/chrome-launcher/-/chrome-launcher-1.2.0.tgz", @@ -3152,6 +3260,62 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz", @@ -3574,6 +3738,19 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/firefox-profile": { "version": "4.7.0", "resolved": "https://registry.npmmirror.com/firefox-profile/-/firefox-profile-4.7.0.tgz", @@ -3775,6 +3952,19 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -3907,6 +4097,16 @@ "entities": "^4.4.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", @@ -3951,6 +4151,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz", @@ -3990,6 +4203,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4000,6 +4223,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-in-ci": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/is-in-ci/-/is-in-ci-1.0.0.tgz", @@ -4046,6 +4282,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-path-inside": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-4.0.0.tgz", @@ -4542,6 +4788,16 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", @@ -4577,6 +4833,25 @@ "node": ">=14.0.0" } }, + "node_modules/onchange": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/onchange/-/onchange-7.1.0.tgz", + "integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blakeembrey/deque": "^1.0.5", + "@blakeembrey/template": "^1.0.0", + "arg": "^4.1.3", + "chokidar": "^3.3.1", + "cross-spawn": "^7.0.1", + "ignore": "^5.1.4", + "tree-kill": "^1.2.2" + }, + "bin": { + "onchange": "dist/bin.js" + } + }, "node_modules/os-shim": { "version": "0.1.3", "resolved": "https://registry.npmmirror.com/os-shim/-/os-shim-0.1.3.tgz", @@ -4645,6 +4920,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", @@ -4878,6 +5163,32 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", @@ -5115,6 +5426,29 @@ "dev": true, "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.7.3", "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.7.3.tgz", @@ -5437,6 +5771,29 @@ "node": ">=14.14" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/type-fest": { "version": "3.13.1", "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-3.13.1.tgz", diff --git a/package.json b/package.json index f73f94a..69d3050 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "main": "main.js", "scripts": { "build": "vite build", + "build:dev": "cross-env NODE_ENV=development vite build --mode development", + "watch:build": "onchange \"features/**/*.js\" \"utils/**/*.js\" \"page/**/*.js\" \"main.js\" \"background.js\" -- npm run build:dev", "dev": "vite dev", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -14,6 +16,8 @@ "description": "", "devDependencies": { "@vitejs/plugin-legacy": "^6.1.1", + "cross-env": "^10.1.0", + "onchange": "^7.1.0", "vite": "^6.3.6", "vite-plugin-web-extension": "^4.4.5" }, diff --git a/page/admin/form/admin-field-label-processor.js b/page/admin/form/admin-field-label-processor.js new file mode 100644 index 0000000..aa9d015 --- /dev/null +++ b/page/admin/form/admin-field-label-processor.js @@ -0,0 +1,81 @@ +/** + * 管理表单页面字段标签处理器模块 + * 负责处理 admin 表单页面上的字段和间距标签 + */ + +/** + * 用于处理 admin 表单页面上字段和间距标签的字段标签处理器类 + */ +export class AdminFieldLabelProcessor { + /** + * 构造函数 + * @param {Object} options - 配置选项 + * @param {number} options.appId - App ID + * @param {string} options.pageType - 页面类型上下文(例如,'admin') + */ + constructor(options = {}) { + this.appId = options.appId; + this.pageType = options.pageType; + console.log(`AdminFieldLabelProcessor initialized with appId: ${this.appId}, pageType: ${this.pageType}`); + } + + /** + * 返回是否为预览模式 + * @returns {boolean} 是否预览 + */ + isPreview() { + return true; + } + + /** + * 为子表字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addSubtableLabel(field, fieldElement) { + console.log(`AdminFieldLabelProcessor.addSubtableLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + } + + /** + * 为引用表字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addReferenceTableLabel(field, fieldElement) { + console.log(`AdminFieldLabelProcessor.addReferenceTableLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + } + + /** + * 为组字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addGroupLabel(field, fieldElement) { + console.log(`AdminFieldLabelProcessor.addGroupLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + } + + /** + * 为标准字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addStandardFieldLabel(field, fieldElement) { + console.log(`AdminFieldLabelProcessor.addStandardFieldLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + } + + /** + * 处理并为页面上的所有字段元素添加标签 + * @param {Array} fieldsWithLabels - 已处理的字段对象数组 + */ + processFieldLabels(fieldsWithLabels) { + console.log(`AdminFieldLabelProcessor.processFieldLabels called with ${fieldsWithLabels.length} fields`); + } + + /** + * 处理并为页面上的间距元素添加标签 + * @param {Array} spacerElements - 间距元素配置数组 + */ + processSpacerLabels(spacerElements) { + console.log(`AdminFieldLabelProcessor.processSpacerLabels called with ${spacerElements.length} spacer elements`); + } +} diff --git a/page/detail/dom.js b/page/detail/dom.js new file mode 100644 index 0000000..814233d --- /dev/null +++ b/page/detail/dom.js @@ -0,0 +1,99 @@ +/** + * DOM 字段标签操作工具模块 + * 提供用于创建和管理字段标签 DOM 元素的辅助函数 + */ + +import { FIELD_TYPES } from '../../utils/constants.js'; +import { COLORS, SPACING, IS_FIELD_TYPE_DISPLAY } from '../../features/add-field-label/settings.js'; + +/** + * 创建带有适当样式的字段标签 span 元素 + * @param {Object} params - 标签参数配置 + * @param {string} params.code - 字段代码 + * @param {string} params.type - 字段类型(可选) + * @param {number} params.width - 字段宽度(可选) + * @returns {HTMLElement} 带有样式化 span 的容器 div 元素 + */ +const createFieldSpanElement = ({ code, type, width }) => { + const container = document.createElement('div'); + + // 处理宽度和边距设置 + if (width !== undefined) { + container.style.width = `${Number(width) - SPACING.TABLE_COLUMN_PADDING}px`; // 减去列填充 + container.style.marginLeft = `${SPACING.TABLE_COLUMN_PADDING}px`; // 添加左边距 + } else { + container.style.width = '100%'; // 默认全宽度 + } + + // 创建和样式化 span 元素 + const fieldSpan = document.createElement('span'); + fieldSpan.textContent = (IS_FIELD_TYPE_DISPLAY && type !== undefined) ? `${code} (${type})` : code; // 显示代码和类型 + fieldSpan.style.display = 'inline-block'; + fieldSpan.style.width = '100%'; + fieldSpan.style.color = COLORS.LABEL_TEXT; // 使用定义的工具提示文本颜色 + fieldSpan.style.overflowWrap = 'anywhere'; // 支持长文本换行 + fieldSpan.style.whiteSpace = 'pre-wrap'; // 保留空格和换行 + + // GROUP 类型字段的特殊样式 + if (type === FIELD_TYPES.GROUP) { + fieldSpan.style.marginLeft = SPACING.GROUP_MARGIN_LEFT; + } + + container.appendChild(fieldSpan); + return container; +}; + +/** + * 创建字段标签容器元素 + * @param {Object} params - 标签参数配置 + * @param {string} params.code - 字段代码 + * @param {string} params.type - 字段类型(可选) + * @param {number} params.width - 容器宽度(可选) + * @returns {HTMLElement} 标签容器元素 + */ +export const createFieldWithLabels = ({ code, type, width }) => { + const container = document.createElement('div'); + container.style.display = 'inline-block'; // 布局的内联块显示 + + const fieldSpan = createFieldSpanElement({ code, type, width }); + container.appendChild(fieldSpan); + + return container; +}; + +/** + * 创建包含多个字段标签的容器 + * @param {Array} fieldNames - 要为其创建标签的字段名称数组 + * @param {Array} widths - 与字段名称对应的宽度 + * @param {string} fieldType - 字段类型(影响间距) + * @returns {HTMLElement} 带有所有标签的 span 容器元素 + */ +export const createFieldLabels = (fieldNames, widths, fieldType, specialSpacing = false) => { + const container = document.createElement('span'); + + // 为引用表字段添加特殊间距 + if (fieldType === FIELD_TYPES.REFERENCE_TABLE && specialSpacing) { + const spacerElement = document.createElement('span'); + spacerElement.style.width = SPACING.REFERENCE_TABLE_SPACER; + spacerElement.style.display = 'inline-block'; + container.appendChild(spacerElement); + } + + // 为每个字段创建标签,调整宽度 + const labelElements = fieldNames.map((fieldName, index) => { + // 稍稍减少最后一个元素的宽度以适应布局 + const adjustedWidth = index === fieldNames.length - 1 + ? widths[index] - 1 + : widths[index]; + + return createFieldWithLabels({ + code: fieldName, + width: adjustedWidth, + }); + }); + + // 将所有标签元素添加到容器中 + labelElements.forEach(labelElement => container.appendChild(labelElement)); + + return container; +}; diff --git a/page/detail/field-label-processor.js b/page/detail/field-label-processor.js new file mode 100644 index 0000000..9e7da3e --- /dev/null +++ b/page/detail/field-label-processor.js @@ -0,0 +1,241 @@ +/** + * 详情页面字段标签处理器模块 + * 负责处理不同页面上的字段和间距标签 + */ + +import { + getColumnWidths, + safelyInsertLabel, + safelyAppendLabel +} from '../../utils/dom-utils.js'; +import { + createFieldWithLabels, + createFieldLabels +} from './dom.js'; +import { FieldTypeChecker } from '../../utils/field-utils.js'; +import { COLORS } from '../../features/add-field-label/settings.js'; +import { LAYOUT_TYPES } from '../../utils/constants.js'; + +/** + * 用于处理不同页面上字段和间距标签的字段标签处理器类 + */ +export class FieldLabelProcessor { + /** + * 构造函数 + * @param {Object} options - 配置选项 + * @param {number} options.appId - App ID + * @param {string} options.pageType - 页面类型上下文(例如,'detail','edit') + */ + constructor(options = {}) { + this.appId = options.appId; + this.pageType = options.pageType; + } + + /** + * 返回是否为预览模式 + * @returns {boolean} 是否预览 + */ + isPreview() { + return false; + } + + /** + * 为子表字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addSubtableLabel(field, fieldElement) { + try { + // 在表上方添加字段代码标签 + const tableCodeLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + tableCodeLabel.style.display = 'block'; + safelyInsertLabel(fieldElement, tableCodeLabel); + // 添加列标签 + const fieldNames = Object.keys(field.fields); + const columnWidths = getColumnWidths(fieldElement, field.type); + const columnLabels = createFieldLabels(fieldNames, columnWidths, field.type); + + safelyInsertLabel(fieldElement, columnLabels); + + } catch (error) { + console.error(`Failed to add label for subtable field ${field.code}:`, error); + } + } + + /** + * 为引用表字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addReferenceTableLabel(field, fieldElement) { + try { + // 在引用表上方添加字段代码标签 + const tableCodeLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + tableCodeLabel.style.display = 'block'; + + safelyInsertLabel(fieldElement, tableCodeLabel); + + // 如果可用,添加显示字段标签 + if (field.referenceTable?.displayFields) { + const displayFields = field.referenceTable.displayFields; + const columnWidths = getColumnWidths(fieldElement, field.type); + const dataExist = !!fieldElement.querySelector('tbody > tr'); + const displayLabels = createFieldLabels(displayFields, columnWidths, field.type, dataExist); + + safelyInsertLabel(fieldElement, displayLabels); + } + + } catch (error) { + console.error(`Failed to add label for reference table field ${field.code}:`, error); + } + } + + /** + * 为组字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addGroupLabel(field, fieldElement) { + try { + // 在组的父元素之前添加标签 + const parentElement = fieldElement.parentElement; + if (parentElement) { + const groupLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + + safelyInsertLabel(parentElement, groupLabel); + } else { + console.warn(`Parent element for group field ${field.code} does not exist`); + } + + } catch (error) { + console.error(`Failed to add label for group field ${field.code}:`, error); + } + } + + /** + * 为标准字段元素添加标签 + * @param {Object} field - 字段配置信息 + * @param {HTMLElement} fieldElement - 字段 DOM 元素 + */ + addStandardFieldLabel(field, fieldElement) { + try { + const fieldLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + + safelyInsertLabel(fieldElement, fieldLabel); + + } catch (error) { + console.error(`Failed to add label for standard field ${field.code}:`, error); + } + } + + /** + * 处理并为页面上的所有字段元素添加标签 + * @param {Array} fieldsWithLabels - 已处理的字段对象数组 + */ + processFieldLabels(fieldsWithLabels) { + try { + // 统计处理结果 + let processedCount = 0; + let skippedCount = 0; + + for (const field of fieldsWithLabels) { + try { + // 获取字段元素 + const fieldElement = kintone.app.record.getFieldElement(field.code); + + if (!fieldElement) { + skippedCount++; + continue; + } + + // 根据字段类型选择合适的标签添加方法 + if (FieldTypeChecker.isSubtable(field)) { + this.addSubtableLabel(field, fieldElement); + } else if (FieldTypeChecker.isReferenceTable(field)) { + this.addReferenceTableLabel(field, fieldElement); + } else if (FieldTypeChecker.isGroup(field)) { + this.addGroupLabel(field, fieldElement); + } else { + this.addStandardFieldLabel(field, fieldElement); + } + + processedCount++; + + } catch (fieldError) { + console.error(`Error occurred while processing label for field ${field.code}:`, fieldError); + skippedCount++; + } + } + + console.log(`Field label processing completed: ${processedCount} successful, ${skippedCount} skipped`); + + } catch (error) { + console.error('Error occurred while processing field labels:', error); + throw error; + } + } + + /** + * 处理并为页面上的间距元素添加标签 + * @param {Array} spacerElements - 间距元素配置数组 + */ + processSpacerLabels(spacerElements) { + try { + // 统计处理结果 + let processedCount = 0; + let skippedCount = 0; + + for (const spacer of spacerElements) { + try { + // 获取间距元素 + const spacerElement = kintone.app.record.getSpaceElement(spacer.elementId); + + if (!spacerElement) { + skippedCount++; + continue; + } + // 添加标签并设置边框 + const spacerLabel = createFieldWithLabels({ + code: spacer.elementId, + type: spacer.type, + }); + + safelyAppendLabel(spacerElement, spacerLabel); + + // 添加红色虚线边框 + spacerElement.style.border = COLORS.SPACER_BORDER; + + processedCount++; + + } catch (spacerError) { + console.error(`Error occurred while processing label for spacer element ${spacer.elementId}:`, spacerError); + skippedCount++; + } + } + + document.querySelectorAll('.spacer-cybozu:not([id])').forEach(spacerElement => { + spacerElement.style.border = COLORS.SPACER_BORDER; + safelyAppendLabel(spacerElement, createFieldWithLabels({ + code: '', + type: LAYOUT_TYPES.SPACER, + })); + }); + + } catch (error) { + console.error('Error occurred while processing spacer element labels:', error); + throw error; + } + } +} diff --git a/utils/constants.js b/utils/constants.js new file mode 100644 index 0000000..bdbe7db --- /dev/null +++ b/utils/constants.js @@ -0,0 +1,107 @@ +// 字段类型常量定义 +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', +}; + +// 布局类型常量定义 +export const LAYOUT_TYPES = { + ROW: 'ROW', + SUBTABLE: 'SUBTABLE', + GROUP: 'GROUP', + LABEL: 'LABEL', + HR: 'HR', + SPACER: 'SPACER', +}; + +// 支持选项排序的字段类型列表 +export const OPTION_SORTABLE_TYPES = [ + FIELD_TYPES.CHECK_BOX, + FIELD_TYPES.DROP_DOWN, + FIELD_TYPES.MULTI_SELECT, + FIELD_TYPES.RADIO_BUTTON, +]; + +// 分组布局中排除的字段类型列表 +export const EXCLUDED_GROUP_TYPES = [ + FIELD_TYPES.CATEGORY, + FIELD_TYPES.STATUS, + FIELD_TYPES.STATUS_ASSIGNEE, + FIELD_TYPES.SUBTABLE, + FIELD_TYPES.GROUP, +]; + +// 系统字段类型列表(自动添加到所有表单中) +export const SYSTEM_FIELD_TYPES = [ + FIELD_TYPES.RECORD_NUMBER, + FIELD_TYPES.CREATOR, + FIELD_TYPES.CREATED_TIME, + FIELD_TYPES.MODIFIER, + FIELD_TYPES.UPDATED_TIME, +]; + +// 系统状态字段类型列表 +export const SYSTEM_STATUS_FIELD_TYPES = [ + FIELD_TYPES.STATUS, + FIELD_TYPES.STATUS_ASSIGNEE, + FIELD_TYPES.CATEGORY, +]; + +// 支持查找复制的字段类型列表 +export const LOOKUP_COPY_SUPPORTED_TYPES = [ + 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, +]; + +// 页面类型常量定义 +export const PAGE_TYPES = { + DETAIL: 'detail', + EDIT: 'edit', + CREATE: 'create', + ADMIN: 'admin', +}; + +export const SCRIPT_FILES = [ + 'main.js' // 立即执行 +]; diff --git a/utils/dom-utils.js b/utils/dom-utils.js new file mode 100644 index 0000000..10d6e7a --- /dev/null +++ b/utils/dom-utils.js @@ -0,0 +1,70 @@ +/** + * DOM 操作工具模块 + * 提供用于创建和管理 DOM 元素的辅助函数 + */ + +import { FIELD_TYPES } from './constants.js'; + +/** + * 从表元素中提取列宽度信息 + * @param {HTMLElement} tableElement - 要分析的表元素 + * @param {string} fieldType - 字段类型(影响返回的宽度数组) + * @returns {Array} 表列宽度的数组 + */ +export const getColumnWidths = (tableElement, fieldType) => { + // 获取所有标题元素并提取它们的客户端宽度 + const headerElements = Array.from(tableElement.querySelectorAll('th')); + const columnWidths = headerElements.map(header => header.clientWidth); + + // 对于子表返回所有宽度,其他表跳过第一列(通常是操作列) + return fieldType === FIELD_TYPES.SUBTABLE + ? columnWidths // 子表保留所有列 + : columnWidths.slice(1); // 其他表跳过第一列 +}; + +/** + * 安全地将标签元素插入到指定目标元素之前 + * @param {HTMLElement} targetElement - 要插入之前的目标元素 + * @param {HTMLElement} labelElement - 要插入的标签元素 + * @returns {boolean} 插入是否成功 + */ +export const safelyInsertLabel = (targetElement, labelElement) => { + try { + if (!targetElement || !labelElement) { + console.warn('Failed to insert label: target element or label element does not exist'); + return false; + } + + if (!targetElement.before) { + console.warn('Failed to insert label: target element does not support before method'); + return false; + } + + targetElement.before(labelElement); + return true; + } catch (error) { + console.error('Error inserting label: ', error); + return false; + } +}; + +/** + * 安全地将标签元素追加到指定目标元素 + * @param {HTMLElement} targetElement - 要追加到的目标元素 + * @param {HTMLElement} labelElement - 要追加的标签元素 + * @returns {boolean} 追加是否成功 + */ +export const safelyAppendLabel = (targetElement, labelElement) => { + try { + if (!targetElement || !labelElement) { + console.warn('Failed to append label: target element or label element does not exist'); + return false; + } + + targetElement.appendChild(labelElement); + return true; + } catch (error) { + console.error('Error appending label: ', error); + return false; + } +}; diff --git a/utils/field-utils.js b/utils/field-utils.js new file mode 100644 index 0000000..d35406f --- /dev/null +++ b/utils/field-utils.js @@ -0,0 +1,85 @@ +/** + * 字段处理工具函数模块 + * 提供字段类型判断、选项排序、系统字段处理等工具函数 + */ + +import { + FIELD_TYPES, + OPTION_SORTABLE_TYPES, + LOOKUP_COPY_SUPPORTED_TYPES +} from './constants.js'; + +/** + * 排序字段选项,根据索引值进行升序排序 + * @param {Object} field - 字段对象 + * @returns {Array} 按索引排序后的选项标签数组 + */ +export 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); +}; + +/** + * 检查字段类型是否支持选项排序 + * @param {string} fieldType - 字段类型 + * @returns {boolean} 是否支持选项排序 + */ +export const shouldSortOptions = (fieldType) => OPTION_SORTABLE_TYPES.includes(fieldType); + +/** + * 标记查找复制字段 + * @param {Array} fields - 要处理的字段数组 + */ +export const markLookupCopies = (fields) => { + fields.forEach(field => { + // 检查字段是否有查找设置 + if (field.lookup?.fieldMappings) { + field.lookup.fieldMappings.forEach(mapping => { + // 在字段数组中找到对应的目标字段并标记为查找复制 + const targetField = fields.find(f => + LOOKUP_COPY_SUPPORTED_TYPES.includes(f.type) && f.code === mapping.field + ); + if (targetField) { + targetField.isLookupCopy = true; + } + }); + } + }); +}; + +export const FieldTypeChecker = { + isCategory: field => field.type === FIELD_TYPES.CATEGORY, + isCheckBox: field => field.type === FIELD_TYPES.CHECK_BOX, + isCreatedTime: field => field.type === FIELD_TYPES.CREATED_TIME, + isCreator: field => field.type === FIELD_TYPES.CREATOR, + isDate: field => field.type === FIELD_TYPES.DATE, + isDatetime: field => field.type === FIELD_TYPES.DATETIME, + isDropDown: field => field.type === FIELD_TYPES.DROP_DOWN, + isFile: field => field.type === FIELD_TYPES.FILE, + isGroup: field => field.type === FIELD_TYPES.GROUP, + isGroupSelect: field => field.type === FIELD_TYPES.GROUP_SELECT, + isInGroup: field => 'group' in field, + isInSubtable: field => 'table' in field, + isLink: field => field.type === FIELD_TYPES.LINK, + isLookup: field => 'lookup' in field, + isLookupCopy: field => field.isLookupCopy === true, + isModifier: field => field.type === FIELD_TYPES.MODIFIER, + isMultiLineText: field => field.type === FIELD_TYPES.MULTI_LINE_TEXT, + isMultiSelect: field => field.type === FIELD_TYPES.MULTI_SELECT, + isNotInSubtable: field => !('table' in field), + isNumber: field => field.type === FIELD_TYPES.NUMBER, + isOrganizationSelect: field => field.type === FIELD_TYPES.ORGANIZATION_SELECT, + isRadioButton: field => field.type === FIELD_TYPES.RADIO_BUTTON, + isRecordNumber: field => field.type === FIELD_TYPES.RECORD_NUMBER, + isReferenceTable: field => field.type === FIELD_TYPES.REFERENCE_TABLE, + isRichText: field => field.type === FIELD_TYPES.RICH_TEXT, + isSingleLineText: field => field.type === FIELD_TYPES.SINGLE_LINE_TEXT, + isStatus: field => field.type === FIELD_TYPES.STATUS, + isStatusAssignee: field => field.type === FIELD_TYPES.STATUS_ASSIGNEE, + isSubtable: field => field.type === FIELD_TYPES.SUBTABLE, + isTime: field => field.type === FIELD_TYPES.TIME, + isUpdatedTime: field => field.type === FIELD_TYPES.UPDATED_TIME, + isUserSelect: field => field.type === FIELD_TYPES.USER_SELECT, +}; diff --git a/utils/kintone-utils.js b/utils/kintone-utils.js new file mode 100644 index 0000000..a04e473 --- /dev/null +++ b/utils/kintone-utils.js @@ -0,0 +1,100 @@ +/** + * Kintone API 工具模块 + * 提供与 Kintone 应用程序交互的通用工具函数 + */ + +/** + * 从当前 URL 中提取 Guest Space ID + * @returns {string|undefined} Guest Space ID,如果未找到则为 undefined + */ +export const getGuestSpaceId = () => { + try { + const match = window.location.pathname.match(/\/guest\/([0-9]+)\//); + return match ? match[1] : undefined; + } catch (error) { + console.warn(error); + return undefined; + } +}; + +const getAppIdFromUrl = () => { + try { + const match = window.location.search.match(/\?app=([0-9]+)/); + return match ? Number(match[1]) : undefined; + } catch (error) { + console.warn(error); + return undefined; + } +}; + +/** + * 从 Kintone 获取当前 APP ID + * @returns {number|null} APP ID,如果没有为 null + */ +export const getAppId = () => { + try { + if (isInAdminPage()) { + return getAppIdFromUrl() || null; + } + + if (!isGlobalKintoneExist()) { + return null; + } + + const appId = kintone.app.getId(); + if (!appId || isNaN(appId)) { + console.warn('Retrieved app ID is invalid:', appId); + return null; + } + + return appId; + } catch (error) { + console.warn('Failed to get app ID: ', error); + return null; + } +}; + + +export const isGlobalKintoneExist = () => { + return typeof kintone !== 'undefined' && typeof kintone.app !== 'undefined'; +}; + +/** + * 是否在 Kintone App 中 + * @returns {boolean} 是否在 App 中 + */ +export const isInAppPage = () => { + return isGlobalKintoneExist() && /^\/k\/\d+/.test(window.location.pathname); +}; + +/** + * 是否在 Kintone App Detail 中 + * @returns {boolean} 是否在 App 中 + */ +export const isInDetailPage = () => { + return isGlobalKintoneExist() && /^\/k\/\d+\/show/.test(window.location.pathname); +}; + +/** + * 是否在 Kintone App 的 /admin 页面 + * @returns {boolean} 是否在 App /admin 页面 + */ +export const isInAdminPage = () => { + return !isGlobalKintoneExist() && window.location.pathname.includes('/k/admin'); +}; + +/** + * 是否在 Kintone App 的 /admin Form 页面 + * @returns {boolean} 是否在 App /admin Form 页面 + */ +export const isInAdminFormPage = () => { + return isInAdminPage() && window.location.hash.includes('#section=form'); +}; + +/** + * 是否在 Kintone 的 space 页面 + * @returns {boolean} 是否在 space 页面 + */ +export const isInSpacePage = () => { + return isGlobalKintoneExist() && window.location.pathname.includes('/k/#/space'); +}; diff --git a/vite.config.js b/vite.config.js index 3c49b63..0e9e80c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,14 +1,32 @@ import { defineConfig } from 'vite'; import webExtension from 'vite-plugin-web-extension'; +import { SCRIPT_FILES } from './utils/constants.js'; + +const isDev = process.env.NODE_ENV === 'development'; export default defineConfig({ plugins: [ webExtension({ - additionalInputs: ['main.js', 'fields.js', 'dom.js'], + // 这个项目中所有需要注入的脚本都需要在这里指定,因为它们不会自动被插件检测到 + // manifest.json 没有指定 content_scripts,所有脚本都通过编程注入 + additionalInputs: SCRIPT_FILES, }), ], build: { - outDir: 'dist', - sourcemap: true, + outDir: 'dist', // 输出目录 + // Chrome扩展不支持source maps,直接禁用以减小包体积 + sourcemap: false, + // 开发模式禁用压缩便于调试,生产模式启用压缩优化体积 + minify: !isDev, + cssMinify: !isDev, + rollupOptions: !isDev ? undefined : { + output: { + // 开发模式下不压缩文件名,便于调试 + chunkFileNames: '[name].js', + entryFileNames: '[name].js', + assetFileNames: '[name].[ext]', + compact: false, // 禁用代码压缩 + }, + }, }, });