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