glib-web 4.40.0 → 4.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.nycrc.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "reporter": [
3
+ "text-summary",
4
+ "html"
5
+ ],
6
+ "report-dir": "./coverage",
7
+ "extension": [
8
+ ".js",
9
+ ".ts",
10
+ ".vue"
11
+ ],
12
+ "exclude": [
13
+ "cypress/**",
14
+ "coverage/**",
15
+ "doc/**",
16
+ "node_modules/**"
17
+ ]
18
+ }
@@ -1,5 +1,7 @@
1
1
  // import Launch from "../../utils/launch";
2
2
 
3
+ import Action from "../../action";
4
+
3
5
  export default class {
4
6
  execute(properties, component) {
5
7
  const spec = Object.assign({}, properties, {
@@ -13,5 +15,7 @@ export default class {
13
15
  ]
14
16
  });
15
17
  Utils.launch.dialog.open(spec, component);
18
+
19
+ Action.execute(properties.onOpen, component);
16
20
  }
17
21
  }
@@ -1,3 +1,4 @@
1
+ import Action from "../../action";
1
2
  import { dialogs } from "../../store";
2
3
 
3
4
  export default class {
@@ -13,5 +14,7 @@ export default class {
13
14
  }
14
15
 
15
16
  Utils.launch.dialog.open(prop, component);
17
+
18
+ Action.execute(spec.onOpen, component);
16
19
  }
17
20
  }
@@ -1,3 +1,4 @@
1
+ import Action from "../../action";
1
2
  import { dialogs } from "../../store";
2
3
 
3
4
  export default class {
@@ -12,5 +13,6 @@ export default class {
12
13
  return;
13
14
  }
14
15
  Utils.launch.dialog.open(prop, component);
16
+ Action.execute(spec.onShow, component);
15
17
  }
16
18
  }
@@ -1,10 +1,10 @@
1
1
  import moment from "moment-timezone";
2
+ import { isString } from "../../utils/type";
2
3
 
3
- export function sanitize(val, type, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
4
-
4
+ export function sanitize(val, type, timeZone) {
5
5
  if (!val) return;
6
-
7
- val = moment(val).tz(timeZone).toISOString(true);
6
+ const hasTimeZone = isString(timeZone) && timeZone !== "";
7
+ val = hasTimeZone ? moment.tz(val, timeZone).toISOString(true) : moment(val).toISOString(true);
8
8
 
9
9
  switch (type) {
10
10
  case "date":
@@ -31,4 +31,4 @@ export function localeString(val, type, format) {
31
31
  return new Date(val).toLocaleDateString(undefined, fmt);
32
32
  }
33
33
 
34
- }
34
+ }
@@ -1,20 +1,28 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="loadIf">
2
+ <div :style="$styles()" :class="$classes()" class="pattern-picker-field" v-if="loadIf">
3
3
  <button-date v-if="spec.template" :type="type" :spec="spec" @datePicked="handleDatePicked"></button-date>
4
4
  <!-- See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date for why we need to use `pattern` -->
5
- <v-text-field v-else ref="field" :color="gcolor" v-model="fieldModel" :name="fieldName" :label="spec.label"
6
- :hint="spec.hint" :type="type" :readonly="spec.readOnly" :disabled="inputDisabled" :min="sanitizeValue(spec.min)"
7
- :max="sanitizeValue(spec.max)" :pattern="pattern" :rules="$validation()" :style="$styles()"
8
- :density="$classes().includes('compact') ? 'compact' : 'default'" :clearable="spec.clearable" @change="onChange"
9
- :variant="variant" validate-on="blur" persistent-placeholder />
5
+
6
+ <template v-else>
7
+ <v-text-field ref="field" :color="gcolor" :label="spec.label" :hint="spec.hint" :readonly="spec.readOnly"
8
+ :disabled="inputDisabled" :style="$styles()" :density="$classes().includes('compact') ? 'compact' : 'default'"
9
+ :clearable="spec.clearable" @change="onChange" :variant="variant" validate-on="blur" persistent-placeholder
10
+ :placeholder="spec.placeholder" @click="openPicker" :value="format" />
11
+ <input ref="nativePicker" v-model="fieldModel" class="native-picker" :type="type" :name="fieldName"
12
+ :min="sanitizeValue(spec.min)" :max="sanitizeValue(spec.max)" :value="sanitizeValue(fieldModel)"
13
+ @change="handleNativeChange" />
14
+ <v-input :model-value="fieldModel" :rules="$validation()"></v-input>
15
+ </template>
10
16
  </div>
11
17
  </template>
12
18
 
13
19
  <script>
20
+ import { useDate } from "vuetify";
14
21
  import { sanitize } from "../composable/date";
15
22
  import { useGlibInput } from "../composable/form";
16
23
  import inputVariant from '../mixins/inputVariant';
17
24
  import buttonDate from "./_buttonDate.vue";
25
+ import { isPresent, isString, isFunction } from "../../utils/type";
18
26
 
19
27
  export default {
20
28
  mixins: [inputVariant],
@@ -25,20 +33,73 @@ export default {
25
33
  pattern: { type: String, required: true }
26
34
  },
27
35
  setup(props) {
36
+ const adapter = useDate();
28
37
  useGlibInput({ props });
38
+
39
+ return { adapter };
40
+ },
41
+ computed: {
42
+ isDateType() {
43
+ return this.type === 'date';
44
+ },
45
+ isDateTimeType() {
46
+ return this.type === 'datetime-local';
47
+ },
48
+ format() {
49
+ if (!isPresent(this.fieldModel)) return;
50
+ if (!isPresent(this.spec.format)) {
51
+ switch (this.type) {
52
+ case 'date':
53
+ return this.adapter.format(this.fieldModel, 'fullDate');
54
+
55
+ case 'datetime-local':
56
+ return this.adapter.format(this.fieldModel, 'keyboardDateTime24h');
57
+
58
+ default:
59
+ return this.adapter.format(this.fieldModel, 'keyboardDateTime24h');
60
+ }
61
+ }
62
+
63
+ return this.adapter.format(this.fieldModel, this.spec.format);
64
+ }
29
65
  },
30
66
  methods: {
31
67
  _linkFieldModels(valueChanged) {
32
68
  if (valueChanged) this.fieldModel = this.sanitizeValue(this.spec.value);
33
69
  },
34
- sanitizeValue(val) {
35
- if (val) {
36
- return sanitize(val, this.type, this.spec.time_zone);
70
+ sanitizeValue(val, timeZone = this.spec.time_zone) {
71
+ if (isPresent(val)) {
72
+ return sanitize(val, this.type, timeZone);
73
+ }
74
+ },
75
+ openPicker() {
76
+ if (this.inputDisabled || this.spec.readOnly) return;
77
+
78
+ const picker = this.$refs.nativePicker;
79
+ if (!picker) return;
80
+
81
+ if (isPresent(this.fieldModel)) {
82
+ picker.value = this.sanitizeValue(this.fieldModel);
83
+ } else if (isPresent(this.spec.value)) {
84
+ picker.value = this.sanitizeValue(this.spec.value);
85
+ }
86
+
87
+ if (isFunction(picker.showPicker)) {
88
+ picker.showPicker();
89
+ } else {
90
+ picker.click();
37
91
  }
38
92
  },
39
93
  onChange() {
40
94
  this.$executeOnChange();
41
95
  },
96
+ handleNativeChange(event) {
97
+ const value = event?.target?.value;
98
+ if (!isString(value)) return;
99
+
100
+ this.fieldModel = this.sanitizeValue(value, '');
101
+ this.$executeOnChange();
102
+ },
42
103
  handleDatePicked(value) {
43
104
  this.fieldModel = value;
44
105
  this.$executeOnChange();
@@ -62,4 +123,17 @@ export default {
62
123
  padding-bottom: unset;
63
124
  }
64
125
  }
126
+
127
+ .pattern-picker-field {
128
+ position: relative;
129
+ }
130
+
131
+ .native-picker {
132
+ position: absolute;
133
+ inset: 0;
134
+ opacity: 0;
135
+ width: 100%;
136
+ height: 100%;
137
+ pointer-events: none;
138
+ }
65
139
  </style>
@@ -15,6 +15,7 @@
15
15
 
16
16
  // Import commands.js using ES2015 syntax:
17
17
  import './commands'
18
+ import '@cypress/code-coverage/support'
18
19
 
19
20
  // Alternatively you can use CommonJS syntax:
20
- // require('./commands')
21
+ // require('./commands')
package/cypress.config.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { defineConfig } from "cypress";
2
+ import codeCoverageTask from "@cypress/code-coverage/task";
2
3
 
3
4
  export default defineConfig({
4
5
  e2e: {
5
6
  setupNodeEvents(on, config) {
6
- // implement node event listeners here
7
+ codeCoverageTask(on, config);
8
+ return config;
7
9
  },
8
10
  },
9
11
  });
package/doc/TESTING.md CHANGED
@@ -54,3 +54,17 @@ You **cannot write frontend tests in isolation**.
54
54
  ---
55
55
 
56
56
  These 6 concepts explain **why the test structure exists** and **what you need to get started**. Everything else (selectors, assertions, etc.) is standard Cypress mechanics.
57
+
58
+ ## Cypress Coverage Workflow
59
+
60
+ - **Prerequisites**: Keep the backend `glib-web` server running with the matching `test_page/*` fixtures (same as normal Cypress runs) and make sure the frontend package is linked into that app: `yarn link glib-web` (run from the backend repo).
61
+ - **Instrument code**: Coverage is only collected when `VITE_COVERAGE=true` because `vite-plugin-istanbul` has `requireEnv` enabled.
62
+ - **Step-by-step run**:
63
+ 1. From the backend repo (`glib-web`), ensure the link is active: `yarn link glib-web`.
64
+ 2. From the backend repo, start Vite with instrumentation: `VITE_COVERAGE=true bin/vite dev`.
65
+ 3. From the backend repo, start Rails: `bin/rails s`.
66
+ 4. From this repo (`glib-web-npm`), execute `yarn test:coverage` to run Cypress and emit coverage reports via `nyc` (uses `.nycrc.json`).
67
+ - **Manual run (if you need custom flags)**:
68
+ 1. `VITE_COVERAGE=true yarn test`
69
+ 2. `nyc report` (or add reporters, e.g. `nyc report --reporter=lcov`).
70
+ - **Where to read results**: HTML report at `coverage/index.html`; summary printed in the terminal; raw data in `.nyc_output` and `coverage/coverage-final.json`.
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
+ "type": "module",
2
3
  "name": "glib-web",
3
- "version": "4.40.0",
4
+ "version": "4.41.1",
4
5
  "description": "",
5
6
  "main": "index.js",
6
7
  "scripts": {
7
- "test": "cypress run --browser chrome"
8
+ "test": "cypress run --browser chrome",
9
+ "test:coverage": "VITE_COVERAGE=true cypress run --browser chrome && nyc report"
8
10
  },
9
11
  "author": "",
10
12
  "license": "ISC",
@@ -42,11 +44,18 @@
42
44
  "vuetify": "3.8.6"
43
45
  },
44
46
  "devDependencies": {
47
+ "@cypress/code-coverage": "^3.14.7",
45
48
  "@types/chart.js": "^2.9.34",
49
+ "@vitejs/plugin-vue": "^6.0.2",
46
50
  "cypress": "^13.13.1",
47
51
  "eslint": "^8.36.0",
48
52
  "eslint-plugin-vue": "^9.26.0",
49
53
  "prettier": "^1.18.2",
50
- "typescript": "^4.9.5"
54
+ "typescript": "^4.9.5",
55
+ "vite": "^7.2.7",
56
+ "vite-plugin-compression": "^0.5.1",
57
+ "vite-plugin-environment": "^1.1.3",
58
+ "vite-plugin-istanbul": "^7.2.1",
59
+ "vite-plugin-ruby": "^5.1.1"
51
60
  }
52
61
  }
@@ -0,0 +1,88 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { defineConfig } from "vite";
4
+ import RubyPlugin from "vite-plugin-ruby";
5
+ import vue from "@vitejs/plugin-vue";
6
+ import viteCompression from "vite-plugin-compression";
7
+ import EnvironmentPlugin from "vite-plugin-environment";
8
+ import istanbul from "vite-plugin-istanbul";
9
+ import * as TypeUtils from "../utils/type.js";
10
+
11
+ function pickStringArray(value) {
12
+ if (!TypeUtils.isArray(value)) {
13
+ return null;
14
+ }
15
+
16
+ const filtered = value.filter((item) => TypeUtils.isString(item));
17
+ return filtered.length > 0 ? filtered : null;
18
+ }
19
+
20
+ function resolveGlibRoot(customRoot) {
21
+ if (TypeUtils.isString(customRoot)) {
22
+ return customRoot;
23
+ }
24
+
25
+ return path.resolve(fileURLToPath(new URL("..", import.meta.url)));
26
+ }
27
+
28
+
29
+ /**
30
+ * Builds the default Vite configuration used by glib projects.
31
+ *
32
+ * @param {object} [options] Configuration overrides.
33
+ * @param {string} [options.glibRoot] Root directory for the glib package (defaults to repo root).
34
+ * @param {string} [options.aliasName] Module alias name for glib imports.
35
+ * @param {string} [options.aliasPath] Filesystem path used for the alias.
36
+ * @param {string} [options.workspaceRoot] Workspace root for coverage and path resolution.
37
+ * @param {string[]} [options.istanbulInclude] Globs to include when running Istanbul.
38
+ * @param {string[]} [options.environmentVariables] Environment variables exposed to the client.
39
+ * @param {string[]} [options.serverAllow] Additional filesystem paths allowed by Vite dev server.
40
+ * @param {import("vite").Plugin[]} [options.plugins] Extra Vite plugins to append.
41
+ * @returns {import("vite").UserConfig} Config object passed to `defineConfig`.
42
+ */
43
+ function useGlibViteDefineConfig(options = {}) {
44
+ if (TypeUtils.isNotNull(globalThis.window)) {
45
+ throw new Error("useGlibViteDefineConfig is only available in Node environments.");
46
+ }
47
+
48
+ const configOptions = TypeUtils.isObject(options) && !TypeUtils.isArray(options) ? options : {};
49
+
50
+ const glibRoot = resolveGlibRoot(configOptions.glibRoot);
51
+ const aliasName = TypeUtils.isString(configOptions.aliasName) ? configOptions.aliasName : "glib-web";
52
+ const aliasPath = TypeUtils.isString(configOptions.aliasPath) ? configOptions.aliasPath : glibRoot;
53
+ const workspaceRoot = TypeUtils.isString(configOptions.workspaceRoot)
54
+ ? configOptions.workspaceRoot
55
+ : path.resolve(glibRoot, "..");
56
+ const istanbulInclude = pickStringArray(configOptions.istanbulInclude) ?? [`${path.basename(glibRoot)}/**/*`];
57
+ const environmentVariables = pickStringArray(configOptions.environmentVariables) ?? ["VITE_GMAPS_API_KEY"];
58
+ const serverAllow = pickStringArray(configOptions.serverAllow) ?? [glibRoot];
59
+ const additionalPlugins = TypeUtils.isArray(configOptions.plugins) ? configOptions.plugins : [];
60
+
61
+ return defineConfig({
62
+ server: {
63
+ fs: {
64
+ allow: serverAllow.map((allowedPath) => path.resolve(allowedPath))
65
+ }
66
+ },
67
+ resolve: {
68
+ alias: {
69
+ [aliasName]: path.resolve(aliasPath)
70
+ }
71
+ },
72
+ plugins: [
73
+ vue(),
74
+ RubyPlugin(),
75
+ istanbul({
76
+ cwd: path.resolve(workspaceRoot),
77
+ include: istanbulInclude,
78
+ requireEnv: true
79
+ }),
80
+ viteCompression(),
81
+ EnvironmentPlugin(environmentVariables, { defineOn: "import.meta.env" }),
82
+ ...additionalPlugins
83
+ ]
84
+ });
85
+ }
86
+
87
+ export { useGlibViteDefineConfig };
88
+ export default useGlibViteDefineConfig;
@@ -11,6 +11,17 @@ const opts = {
11
11
  // blueprint: md3,
12
12
  components,
13
13
  // directives,
14
+ date: {
15
+ formats: {
16
+ glibDate: (d) => {
17
+ const yyyy = d.getFullYear();
18
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
19
+ const dd = String(d.getDate()).padStart(2, "0");
20
+
21
+ return `${yyyy} ${mm} ${dd}`;
22
+ }
23
+ }
24
+ },
14
25
  icons: {
15
26
  defaultSet: 'md',
16
27
  aliases,
@@ -24,4 +35,4 @@ const opts = {
24
35
  }
25
36
  };
26
37
 
27
- export default new createVuetify(opts);
38
+ export default new createVuetify(opts);
package/utils/type.js CHANGED
@@ -1,4 +1,4 @@
1
- import Hash from "./hash";
1
+ import Hash from "./hash.js";
2
2
 
3
3
  export function isObject(obj) {
4
4
  return typeof obj === "object" && obj !== null;