This commit is contained in:
2025-10-17 14:39:35 +08:00
parent 39cc4f4c2e
commit 411f068d75
18 changed files with 1497 additions and 646 deletions

View File

@@ -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<void>} Resolves when all scripts are injected successfully
* Chrome 扩展后台脚本
* 负责将内容脚本注入 Kintone 页面
*/
import { SCRIPT_FILES } from './utils/constants.js';
/**
* 将 Kintone Helper 脚本注入指定标签页
* @param {Object} tab - Chrome 标签页对象
* @returns {Promise<void>} 当脚本注入成功时解决
*/
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);
});

View File

@@ -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,
];

112
dom.js
View File

@@ -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);
};

View File

@@ -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 };
};

View File

@@ -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<Object>} 表单字段和布局数据
*/
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<void>}
*/
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 表单页面
});
}
};

View File

@@ -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;

257
fields.js
View File

@@ -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",
};

186
main.js
View File

@@ -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<Object>} 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<void>}
*/
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();

357
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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`);
}
}

99
page/detail/dom.js Normal file
View File

@@ -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<string>} fieldNames - 要为其创建标签的字段名称数组
* @param {Array<number>} 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;
};

View File

@@ -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;
}
}
}

107
utils/constants.js Normal file
View File

@@ -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' // 立即执行
];

70
utils/dom-utils.js Normal file
View File

@@ -0,0 +1,70 @@
/**
* DOM 操作工具模块
* 提供用于创建和管理 DOM 元素的辅助函数
*/
import { FIELD_TYPES } from './constants.js';
/**
* 从表元素中提取列宽度信息
* @param {HTMLElement} tableElement - 要分析的表元素
* @param {string} fieldType - 字段类型(影响返回的宽度数组)
* @returns {Array<number>} 表列宽度的数组
*/
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;
}
};

85
utils/field-utils.js Normal file
View File

@@ -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,
};

100
utils/kintone-utils.js Normal file
View File

@@ -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');
};

View File

@@ -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, // 禁用代码压缩
},
},
},
});